
I've built 20+ Shopify apps for clients—from simple product sync tools to complex inventory management systems processing thousands of orders daily. In this guide, I'll show you exactly how to build production-ready Shopify apps using Node.js, the Shopify API, and modern development practices.
Why Build Shopify Apps?
For Merchants: Solve specific business problems not addressed by existing apps For Agencies: Offer custom solutions to clients For Developers: Build SaaS products sold in the Shopify App Store For Businesses: Internal tools for custom workflows
I recently built a custom inventory sync app for a multi-location retailer that saved them $2,400/year vs existing solutions while adding features tailored to their workflow.
Types of Shopify Apps
Public Apps
- Listed in Shopify App Store
- Installed by any merchant
- Require app approval process
- Monetization through Shopify billing
Private Apps (Custom Apps)
- Used by single store
- Faster development (no review process)
- Direct API access
- No Shopify billing required
Sales Channel Apps
- Custom storefronts
- Marketplace integrations
- Complex, advanced use case
This guide focuses on public embedded apps (most common), but concepts apply to all types.
Prerequisites
You'll need:
- Node.js 18+ installed
- Shopify Partner account (free at partners.shopify.com)
- Development store (created in Partner dashboard)
- Basic knowledge of JavaScript, React, and REST APIs
Project Setup
Step 1: Install Shopify CLI
The fastest way to scaffold a Shopify app:
npm install -g @shopify/cli @shopify/app
Step 2: Create New App
shopify app create node
# Answer prompts:
# App name: my-shopify-app
# Template: Node.js + React (recommended)
This creates:
- Express.js backend
- React frontend with Polaris components
- OAuth authentication
- App Bridge integration
- Webhook handling
- Database setup (SQLite for development)
Step 3: Configure App in Partner Dashboard
- Go to partners.shopify.com
- Click "Apps" → "Create app"
- Choose "Public app" or "Custom app"
- Enter app name and URL (use ngrok for local development)
Get credentials:
- API key (SHOPIFY_API_KEY)
- API secret (SHOPIFY_API_SECRET)
Add to .env:
SHOPIFY_API_KEY=your_api_key_here
SHOPIFY_API_SECRET=your_api_secret_here
SHOP=your-dev-store.myshopify.com
SCOPES=write_products,read_orders
HOST=https://your-ngrok-url.ngrok.io
Step 4: Start Development Server
# Start app with Shopify CLI (includes ngrok tunnel)
shopify app dev
# Or manually:
npm run dev
Visit your development store, install the app, and you're ready to code!
Understanding Shopify App Architecture
┌─────────────────────────────────────────┐
│ Shopify Admin │
│ ┌───────────────────────────────────┐ │
│ │ Your App (iFrame via App Bridge) │ │
│ │ React Frontend (Polaris) │ │
│ └────────────┬──────────────────────┘ │
└───────────────┼─────────────────────────┘
│
│ API Requests
↓
┌──────────────────────┐
│ Your Backend │
│ (Node.js/Express) │
└──────────┬───────────┘
│
│ Shopify API Calls
↓
┌──────────────────────┐
│ Shopify API │
│ (REST / GraphQL) │
└──────────────────────┘
OAuth Authentication Flow
Shopify uses OAuth 2.0 for secure app installation.
Backend Authentication (server/index.js)
import '@shopify/shopify-api/adapters/node';
import { shopifyApi, ApiVersion, LATEST_API_VERSION } from '@shopify/shopify-api';
import express from 'express';
const shopify = shopifyApi({
apiKey: process.env.SHOPIFY_API_KEY,
apiSecretKey: process.env.SHOPIFY_API_SECRET,
scopes: ['write_products', 'read_orders'],
hostName: process.env.HOST.replace(/https?:\/\//, ''),
apiVersion: LATEST_API_VERSION,
isEmbeddedApp: true, // App runs inside Shopify admin
logger: { level: 'info' }
});
const app = express();
// OAuth - Start authentication
app.get('/api/auth', async (req, res) => {
const { shop } = req.query;
if (!shop) {
return res.status(400).send('Missing shop parameter');
}
// Redirect to Shopify OAuth
const authRoute = await shopify.auth.begin({
shop,
callbackPath: '/api/auth/callback',
isOnline: false,
});
res.redirect(authRoute);
});
// OAuth - Handle callback
app.get('/api/auth/callback', async (req, res) => {
try {
const callback = await shopify.auth.callback({
rawRequest: req,
rawResponse: res,
});
const { session } = callback;
// Save session to database
await saveSession(session);
// Redirect to app
const redirectUrl = `/?shop=${session.shop}&host=${req.query.host}`;
res.redirect(redirectUrl);
} catch (error) {
console.error('Auth callback error:', error);
res.status(500).send('Authentication failed');
}
});
// Verify requests are from authenticated merchants
async function verifyRequest(req, res, next) {
const { shop } = req.query;
if (!shop) {
return res.status(401).send('Unauthorized');
}
const session = await getSession(shop);
if (!session) {
return res.redirect(`/api/auth?shop=${shop}`);
}
req.session = session;
next();
}
// Protected route example
app.get('/api/products', verifyRequest, async (req, res) => {
const client = new shopify.clients.Rest({
session: req.session
});
const products = await client.get({
path: 'products',
});
res.json(products.body.products);
});
app.listen(3000, () => {
console.log('App running on port 3000');
});
Making API Requests
REST API
Shopify's REST API is straightforward for CRUD operations.
import { shopifyApi } from '@shopify/shopify-api';
// Initialize client
const client = new shopify.clients.Rest({
session: req.session // From authentication
});
// Get products
const products = await client.get({
path: 'products',
query: { limit: 50 }
});
console.log(products.body.products);
// Create product
const newProduct = await client.post({
path: 'products',
data: {
product: {
title: 'New Product',
body_html: '<strong>Great product!</strong>',
vendor: 'My Company',
product_type: 'Electronics',
variants: [
{
price: '99.99',
sku: 'PROD-001'
}
]
}
}
});
// Update product
const updated = await client.put({
path: `products/${productId}`,
data: {
product: {
title: 'Updated Title'
}
}
});
// Delete product
await client.delete({
path: `products/${productId}`
});
GraphQL API (Recommended for Complex Queries)
GraphQL is more efficient for fetching related data.
const graphqlClient = new shopify.clients.Graphql({
session: req.session
});
// Query products with variants and images
const productsQuery = `
query getProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
variants(first: 10) {
edges {
node {
id
title
price
inventoryQuantity
}
}
}
images(first: 1) {
edges {
node {
url
altText
}
}
}
}
}
}
}
`;
const response = await graphqlClient.query({
data: {
query: productsQuery,
variables: {
first: 50
}
}
});
const products = response.body.data.products.edges.map(edge => edge.node);
console.log(products);
Mutation example:
const createProductMutation = `
mutation createProduct($input: ProductInput!) {
productCreate(input: $input) {
product {
id
title
handle
}
userErrors {
field
message
}
}
}
`;
const response = await graphqlClient.query({
data: {
query: createProductMutation,
variables: {
input: {
title: 'New Product',
productType: 'Electronics',
variants: [
{
price: '99.99'
}
]
}
}
}
});
if (response.body.data.productCreate.userErrors.length > 0) {
console.error('Errors:', response.body.data.productCreate.userErrors);
} else {
console.log('Product created:', response.body.data.productCreate.product);
}
Webhooks
Webhooks notify your app of events in the store (new order, product update, etc.).
Register Webhooks
import { DeliveryMethod } from '@shopify/shopify-api';
// Register webhook during app installation
await shopify.webhooks.register({
session,
topic: 'ORDERS_CREATE',
deliveryMethod: DeliveryMethod.Http,
callbackUrl: '/api/webhooks/orders_create'
});
// Other useful webhook topics:
// - PRODUCTS_CREATE
// - PRODUCTS_UPDATE
// - PRODUCTS_DELETE
// - CUSTOMERS_CREATE
// - APP_UNINSTALLED (critical for cleanup)
Handle Webhook Requests
app.post('/api/webhooks/orders_create', async (req, res) => {
try {
// Verify webhook authenticity
const isValid = await shopify.webhooks.validate({
rawBody: req.body,
rawRequest: req,
rawResponse: res
});
if (!isValid) {
console.error('Invalid webhook request');
return res.status(401).send('Unauthorized');
}
const order = req.body;
console.log('New order received:', order.id);
// Process order
await processNewOrder(order);
res.status(200).send('Webhook processed');
} catch (error) {
console.error('Webhook error:', error);
res.status(500).send('Internal error');
}
});
async function processNewOrder(order) {
// Example: Send notification, sync to external system, etc.
console.log(`Order #${order.order_number} for ${order.total_price} ${order.currency}`);
// Save to database, send email, update inventory, etc.
}
Important: Register APP_UNINSTALLED webhook to clean up data when merchants uninstall.
Frontend with React and Polaris
Shopify's Polaris design system makes your app look native.
Basic App Structure
// frontend/App.jsx
import { AppProvider, Page, Card, Button } from '@shopify/polaris';
import { Provider } from '@shopify/app-bridge-react';
import '@shopify/polaris/build/esm/styles.css';
export default function App() {
const config = {
apiKey: process.env.SHOPIFY_API_KEY,
host: new URLSearchParams(window.location.search).get('host'),
forceRedirect: true
};
return (
<Provider config={config}>
<AppProvider>
<Page title="My Shopify App">
<Card sectioned>
<p>Welcome to my app!</p>
</Card>
</Page>
</AppProvider>
</Provider>
);
}
Fetching Data from Your Backend
import { useEffect, useState } from 'react';
import { authenticatedFetch } from '@shopify/app-bridge-utils';
import { useAppBridge } from '@shopify/app-bridge-react';
export default function ProductsList() {
const app = useAppBridge();
const fetch = authenticatedFetch(app);
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadProducts();
}, []);
async function loadProducts() {
try {
const response = await fetch('/api/products');
const data = await response.json();
setProducts(data);
} catch (error) {
console.error('Error loading products:', error);
} finally {
setLoading(false);
}
}
if (loading) {
return <Spinner />;
}
return (
<Page title="Products">
<Card>
<ResourceList
resourceName={{ singular: 'product', plural: 'products' }}
items={products}
renderItem={(product) => {
const { id, title, images } = product;
const media = <Thumbnail source={images[0]?.src} alt={title} />;
return (
<ResourceItem id={id} media={media}>
<h3>{title}</h3>
</ResourceItem>
);
}}
/>
</Card>
</Page>
);
}
App Bridge Actions
App Bridge lets your app interact with Shopify admin.
Toast Notifications
import { Toast } from '@shopify/app-bridge-react';
function MyComponent() {
const [showToast, setShowToast] = useState(false);
const toggleToast = () => setShowToast(!showToast);
return (
<>
<Button onClick={toggleToast}>Show Toast</Button>
{showToast && (
<Toast
content="Product saved successfully"
onDismiss={toggleToast}
/>
)}
</>
);
}
Resource Picker
import { ResourcePicker } from '@shopify/app-bridge-react';
function SelectProducts() {
const [pickerOpen, setPickerOpen] = useState(false);
const handleSelection = (resources) => {
console.log('Selected products:', resources.selection);
setPickerOpen(false);
};
return (
<>
<Button onClick={() => setPickerOpen(true)}>
Select Products
</Button>
<ResourcePicker
resourceType="Product"
open={pickerOpen}
onSelection={handleSelection}
onCancel={() => setPickerOpen(false)}
/>
</>
);
}
Context Bar (Save/Discard)
import { ContextualSaveBar } from '@shopify/app-bridge-react';
function EditForm() {
const [isDirty, setIsDirty] = useState(false);
const handleSave = async () => {
// Save changes
await saveData();
setIsDirty(false);
};
const handleDiscard = () => {
// Discard changes
setIsDirty(false);
};
return (
<>
{isDirty && (
<ContextualSaveBar
message="Unsaved changes"
saveAction={{
onAction: handleSave,
}}
discardAction={{
onAction: handleDiscard,
}}
/>
)}
<TextField
label="Product title"
onChange={() => setIsDirty(true)}
/>
</>
);
}
Billing and Monetization
Set Up App Charges
// Create recurring charge
async function createSubscription(session) {
const client = new shopify.clients.Rest({ session });
const charge = await client.post({
path: 'recurring_application_charges',
data: {
recurring_application_charge: {
name: 'Premium Plan',
price: 29.99,
trial_days: 7,
test: true, // Set false for production
return_url: `https://${process.env.HOST}/api/billing/callback`
}
}
});
return charge.body.recurring_application_charge.confirmation_url;
}
// Redirect merchant to approve charge
app.get('/api/billing/subscribe', verifyRequest, async (req, res) => {
const confirmationUrl = await createSubscription(req.session);
res.redirect(confirmationUrl);
});
// Handle billing callback
app.get('/api/billing/callback', verifyRequest, async (req, res) => {
const { charge_id } = req.query;
const client = new shopify.clients.Rest({ session: req.session });
const charge = await client.get({
path: `recurring_application_charges/${charge_id}`
});
if (charge.body.recurring_application_charge.status === 'accepted') {
// Activate charge
await client.post({
path: `recurring_application_charges/${charge_id}/activate`
});
// Update database: merchant is now subscribed
await updateSubscriptionStatus(req.session.shop, 'active');
res.redirect('/settings?subscribed=true');
} else {
res.redirect('/settings?subscribed=false');
}
});
Database Schema
Store app data efficiently:
// Using Prisma ORM (recommended)
// schema.prisma
model Session {
id String @id
shop String @unique
accessToken String
scope String
isOnline Boolean
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Subscription {
id String @id @default(uuid())
shop String @unique
plan String
status String // active, cancelled, expired
chargeId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Settings {
id String @id @default(uuid())
shop String @unique
data Json // Store app-specific settings
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Testing
Unit Tests
import { describe, it, expect } from 'vitest';
import { processOrder } from './utils';
describe('Order Processing', () => {
it('should calculate total correctly', () => {
const order = {
line_items: [
{ price: '10.00', quantity: 2 },
{ price: '5.00', quantity: 1 }
]
};
const total = processOrder(order);
expect(total).toBe(25.00);
});
});
Integration Tests
import request from 'supertest';
import app from './server';
describe('API Endpoints', () => {
it('GET /api/products returns products', async () => {
const response = await request(app)
.get('/api/products')
.set('Authorization', 'Bearer test_token');
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('products');
});
});
Deployment
Deploy to Production
Popular hosting options:
- Heroku (easiest for beginners)
- Railway (modern alternative to Heroku)
- Vercel (great for Next.js apps)
- AWS/GCP (for advanced users)
Heroku deployment:
# Install Heroku CLI
npm install -g heroku
# Login
heroku login
# Create app
heroku create my-shopify-app
# Set env variables
heroku config:set SHOPIFY_API_KEY=your_key
heroku config:set SHOPIFY_API_SECRET=your_secret
# Deploy
git push heroku main
# Open app
heroku open
Update App URLs in Partner Dashboard
- Go to Partner Dashboard → Your App
- Update "App URL" to production URL
- Update "Allowed redirection URL(s)"
- Save changes
Security Best Practices
✅ Never expose API credentials (use environment variables) ✅ Validate all webhook requests (verify HMAC signature) ✅ Use HTTPS in production (required by Shopify) ✅ Implement rate limiting (prevent abuse) ✅ Sanitize user input (prevent XSS/injection) ✅ Store access tokens securely (encrypted database) ✅ Handle APP_UNINSTALLED webhook (delete merchant data) ✅ Follow GDPR/data privacy laws (delete data on request)
Common Issues and Solutions
Issue 1: "Unauthorized" Error
Cause: Session expired or invalid
Fix: Refresh OAuth token or re-authenticate
Issue 2: Webhooks Not Firing
Causes:
- Webhook URL not publicly accessible
- Webhook not registered correctly
- Shopify can't reach your server
Fix: Use ngrok for local testing, check webhook logs in Partner Dashboard
Issue 3: API Rate Limiting
Shopify limits: 2 requests/second (REST), 1000 points/second (GraphQL)
Solution: Implement exponential backoff retry logic
async function makeAPICallWithRetry(apiCall, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await apiCall();
} catch (error) {
if (error.response?.status === 429) {
// Rate limited
const retryAfter = parseInt(error.response.headers['retry-after']) || (2 ** i);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
} else {
throw error;
}
}
}
}
Frequently Asked Questions
Can I build apps with other languages/frameworks?
Yes! Shopify has SDKs for:
- Ruby (Rails)
- PHP (Laravel)
- Python (Django/Flask)
But Node.js is most supported.
How do I get my app approved for the App Store?
Requirements:
- Follow Shopify App Store requirements
- Provide clear documentation
- Offer customer support
- Pass security review
- Demonstrate stable functionality
Approval takes 1-4 weeks.
Can I charge for my app?
Yes, via:
- Recurring charges (monthly subscription)
- One-time charges (single payment)
- Usage charges (based on usage metrics)
Shopify takes 20% of first $1M revenue, 0% after that (as of 2025).
Do I need a custom app or public app?
Custom app: Single merchant, faster development Public app: Multiple merchants, more complex, monetizable
Next Steps
Now you can:
- Build real features: Inventory sync, custom reporting, automation
- Add advanced functionality: Background jobs, email notifications
- Monetize: Launch in App Store or sell to clients
- Scale: Optimize performance for high-traffic stores
Resources
- Shopify App Development Docs
- Shopify CLI Documentation
- Polaris Design System
- Shopify Community Forums
- App Bridge Documentation
Need help building a custom Shopify app? I've developed 20+ Shopify apps for clients worldwide, from inventory management to marketplace integrations. Contact me for custom app development services.

Fysal Yaqoob
Expert WordPress & Shopify Developer
Senior full-stack developer with 10+ years experience specializing in WordPress, Shopify, and headless CMS solutions. Delivering custom themes, plugins, e-commerce stores, and scalable web applications.
Practice: Code Typer
Master WooCommerce development by practicing with real code snippets in Code Typer!

