Shopify App Development with Node.js: Complete Guide for 2025

Learn how to build custom Shopify apps with Node.js and React. Complete guide covering authentication, API integration, webhooks, and App Bridge for public and private apps.

Fysal Yaqoob
18 min
#Shopify#Node.js#React#App Development#API#E-commerce#JavaScript
Shopify App Development with Node.js: Complete Guide for 2025 - Featured image for E-commerce guide

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

  1. Go to partners.shopify.com
  2. Click "Apps" → "Create app"
  3. Choose "Public app" or "Custom app"
  4. 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 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

  1. Go to Partner Dashboard → Your App
  2. Update "App URL" to production URL
  3. Update "Allowed redirection URL(s)"
  4. 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:

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:

  1. Build real features: Inventory sync, custom reporting, automation
  2. Add advanced functionality: Background jobs, email notifications
  3. Monetize: Launch in App Store or sell to clients
  4. Scale: Optimize performance for high-traffic stores

Resources


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

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.

10+ Years500+ Projects100+ Agencies

Practice: Code Typer

Master WooCommerce development by practicing with real code snippets in Code Typer!

Easy3-5 min
Play Now