Skip to main content
@shopify/shopify-app-express provides Express middleware and utilities for building Shopify apps. It wraps @shopify/shopify-api with Express-specific helpers for OAuth, session management, and request handling.

Installation

npm install @shopify/shopify-app-express express
Requires Node.js >= 20.0.0 and Express >= 5.0.0

Quick Start

1. Initialize the App

import express from "express";
import { shopifyApp } from "@shopify/shopify-app-express";
import { PostgreSQLSessionStorage } from "@shopify/shopify-app-session-storage-postgresql";
import { ApiVersion } from "@shopify/shopify-api";

const app = express();

const shopify = shopifyApp({
  api: {
    apiVersion: ApiVersion.January24,
    // Other API config is read from environment variables:
    // SHOPIFY_API_KEY, SHOPIFY_API_SECRET, SCOPES, HOST
  },
  auth: {
    path: "/auth",
    callbackPath: "/auth/callback",
  },
  webhooks: {
    path: "/webhooks",
  },
  sessionStorage: new PostgreSQLSessionStorage(
    process.env.DATABASE_URL
  ),
});

export default shopify;

2. Add Authentication Routes

import shopify from "./shopify";

// OAuth routes
app.get("/auth", shopify.auth.begin());
app.get("/auth/callback", shopify.auth.callback());

// Start OAuth if not installed
app.get("/", shopify.ensureInstalledOnShop(), async (req, res) => {
  res.send("App is installed!");
});

3. Protect Your Routes

import shopify from "./shopify";

// Require valid session
app.get(
  "/api/products",
  shopify.validateAuthenticatedSession(),
  async (req, res) => {
    const session = res.locals.shopify.session;
    
    const client = new shopify.api.clients.Graphql({ session });
    const response = await client.request(
      `query { products(first: 10) { edges { node { id title } } } }`
    );
    
    const data = await response.json();
    res.json(data.data.products);
  }
);

4. Handle Webhooks

app.post(
  "/webhooks",
  express.text({ type: "*/*" }),
  shopify.processWebhooks({ webhookHandlers: {} })
);

Configuration

shopifyApp Options

api
object
required
API configuration object. Most values are read from environment variables.Required Environment Variables:
  • SHOPIFY_API_KEY: Your app’s API key
  • SHOPIFY_API_SECRET: Your app’s API secret
  • SCOPES: Comma-separated OAuth scopes
  • HOST: Your app’s URL (e.g., https://myapp.com)
Required in code:
  • apiVersion: API version (e.g., ApiVersion.January24)
auth
object
required
Authentication configuration.
auth: {
  path: "/auth",              // OAuth start path
  callbackPath: "/auth/callback", // OAuth callback path
}
webhooks
object
required
Webhook configuration.
webhooks: {
  path: "/webhooks",  // Webhook endpoint path
}
sessionStorage
SessionStorage
Session storage adapter. Defaults to in-memory storage (not for production).See Session Storage below.
useOnlineTokens
boolean
default:"false"
Whether to use online access tokens.
exitIframePath
string
default:"/exitiframe"
Path for the exit iframe helper page.

API Configuration

The api object accepts all parameters from @shopify/shopify-api:
const shopify = shopifyApp({
  api: {
    apiVersion: ApiVersion.January24,
    
    // Optional: override environment variables
    apiKey: "custom-api-key",
    apiSecretKey: "custom-secret",
    scopes: ["read_products", "write_orders"],
    hostName: "myapp.example.com",
    
    // Optional: REST resources
    restResources: await import(
      "@shopify/shopify-api/rest/admin/2024-01"
    ),
    
    // Optional: billing
    billing: {
      "My Plan": {
        amount: 10.0,
        currencyCode: "USD",
        interval: BillingInterval.Every30Days,
      },
    },
  },
  // ... other config
});

Middleware

auth.begin()

Starts the OAuth flow:
app.get("/auth", shopify.auth.begin());
Can accept configuration overrides:
app.get("/auth", shopify.auth.begin({
  isOnline: true, // Request online token
}));

auth.callback()

Handles OAuth callback:
app.get("/auth/callback", shopify.auth.callback());
After successful OAuth, redirects to / by default.

validateAuthenticatedSession()

Ensures request has valid session:
app.get(
  "/api/products",
  shopify.validateAuthenticatedSession(),
  async (req, res) => {
    // Access session
    const session = res.locals.shopify.session;
    
    // Use API
    const client = new shopify.api.clients.Graphql({ session });
    // ...
  }
);
The session is available at res.locals.shopify.session

ensureInstalledOnShop()

Redirects to OAuth if shop doesn’t have valid session:
app.get("/", shopify.ensureInstalledOnShop(), (req, res) => {
  res.send("App is installed!");
});

processWebhooks()

Processes incoming webhooks:
const webhookHandlers = {
  PRODUCTS_CREATE: async (topic, shop, body, webhookId) => {
    const payload = JSON.parse(body);
    console.log(`Product created: ${payload.id}`);
  },
  APP_UNINSTALLED: async (topic, shop, body) => {
    // Clean up shop data
    await cleanupShopData(shop);
  },
};

app.post(
  "/webhooks",
  express.text({ type: "*/*" }),
  shopify.processWebhooks({ webhookHandlers })
);
Always use express.text({ type: "*/*" }) before webhook processing to get raw body for HMAC validation.

cspHeaders()

Adds Content Security Policy headers for embedded apps:
app.use(shopify.cspHeaders());

redirectToShopifyOrAppRoot()

Handles redirects for embedded apps:
app.get("/redirect", 
  shopify.redirectToShopifyOrAppRoot()
);

Making API Requests

GraphQL Client

import { GraphqlQueryError } from "@shopify/shopify-api";

app.post(
  "/api/update-product",
  shopify.validateAuthenticatedSession(),
  async (req, res) => {
    const session = res.locals.shopify.session;
    const client = new shopify.api.clients.Graphql({ session });
    
    try {
      const response = await client.request(
        `#graphql
          mutation updateProduct($input: ProductInput!) {
            productUpdate(input: $input) {
              product {
                id
                title
              }
              userErrors {
                field
                message
              }
            }
          }
        `,
        {
          variables: {
            input: {
              id: req.body.productId,
              title: req.body.title,
            },
          },
        }
      );
      
      const data = await response.json();
      res.json(data.data.productUpdate);
    } catch (error) {
      if (error instanceof GraphqlQueryError) {
        res.status(500).json({ errors: error.body?.errors });
      } else {
        res.status(500).send("Error updating product");
      }
    }
  }
);

REST Client

app.get(
  "/api/products",
  shopify.validateAuthenticatedSession(),
  async (req, res) => {
    const session = res.locals.shopify.session;
    const client = new shopify.api.clients.Rest({ session });
    
    const response = await client.get({
      path: "products",
      query: { limit: 10 },
    });
    
    res.json(response.body.products);
  }
);

REST Resources (Type-Safe)

import { restResources } from "@shopify/shopify-api/rest/admin/2024-01";

const shopify = shopifyApp({
  api: {
    restResources,
    // ... other config
  },
});

app.get(
  "/api/products",
  shopify.validateAuthenticatedSession(),
  async (req, res) => {
    const session = res.locals.shopify.session;
    
    // Type-safe REST resources
    const products = await shopify.api.rest.Product.all({
      session,
      limit: 10,
    });
    
    res.json(products);
  }
);

Webhooks

Configure Webhooks

import { DeliveryMethod } from "@shopify/shopify-api";

const webhookHandlers = {
  PRODUCTS_CREATE: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/webhooks",
    callback: async (topic, shop, body, webhookId) => {
      const product = JSON.parse(body);
      console.log(`New product: ${product.title}`);
    },
  },
  ORDERS_PAID: {
    deliveryMethod: DeliveryMethod.Http,
    callbackUrl: "/webhooks",
    callback: async (topic, shop, body) => {
      // Handle order paid
    },
  },
};

app.post(
  "/webhooks",
  express.text({ type: "*/*" }),
  shopify.processWebhooks({ webhookHandlers })
);

Webhook Validation

Webhooks are automatically validated by processWebhooks(). Manual validation:
app.post("/webhooks", express.text({ type: "*/*" }), async (req, res) => {
  const isValid = await shopify.api.webhooks.validate({
    rawBody: req.body,
    rawRequest: req,
  });
  
  if (!isValid) {
    return res.status(401).send("Unauthorized");
  }
  
  // Process webhook
  await shopify.api.webhooks.process({
    rawBody: req.body,
    rawRequest: req,
  });
  
  res.status(200).send();
});

Session Storage

Choose a session storage adapter:
npm install @shopify/shopify-app-session-storage-postgresql
import { PostgreSQLSessionStorage } from "@shopify/shopify-app-session-storage-postgresql";

const shopify = shopifyApp({
  sessionStorage: new PostgreSQLSessionStorage(
    process.env.DATABASE_URL
  ),
  // ... other config
});

Billing

Configure Billing Plans

import { BillingInterval } from "@shopify/shopify-api";

const shopify = shopifyApp({
  api: {
    billing: {
      "Basic Plan": {
        amount: 5.0,
        currencyCode: "USD",
        interval: BillingInterval.Every30Days,
      },
      "Premium Plan": {
        amount: 10.0,
        currencyCode: "USD",
        interval: BillingInterval.Every30Days,
        trialDays: 7,
      },
    },
  },
  // ... other config
});

Request Billing

app.get(
  "/billing/subscribe",
  shopify.validateAuthenticatedSession(),
  async (req, res) => {
    const session = res.locals.shopify.session;
    
    const response = await shopify.api.billing.request({
      session,
      plan: "Premium Plan",
      isTest: true,
    });
    
    res.redirect(response.confirmationUrl);
  }
);

Check Billing Status

app.use(
  shopify.validateAuthenticatedSession(),
  async (req, res, next) => {
    const session = res.locals.shopify.session;
    
    const hasActivePayment = await shopify.api.billing.check({
      session,
      plans: ["Basic Plan", "Premium Plan"],
      isTest: true,
    });
    
    if (!hasActivePayment) {
      return res.redirect("/billing/subscribe");
    }
    
    next();
  }
);

Complete Example

import express from "express";
import { shopifyApp } from "@shopify/shopify-app-express";
import { PostgreSQLSessionStorage } from "@shopify/shopify-app-session-storage-postgresql";
import { ApiVersion, DeliveryMethod } from "@shopify/shopify-api";

const app = express();

const shopify = shopifyApp({
  api: {
    apiVersion: ApiVersion.January24,
  },
  auth: {
    path: "/auth",
    callbackPath: "/auth/callback",
  },
  webhooks: {
    path: "/webhooks",
  },
  sessionStorage: new PostgreSQLSessionStorage(
    process.env.DATABASE_URL
  ),
});

// Add CSP headers for embedded apps
app.use(shopify.cspHeaders());

// OAuth routes
app.get("/auth", shopify.auth.begin());
app.get("/auth/callback", shopify.auth.callback());

// Webhooks
const webhookHandlers = {
  APP_UNINSTALLED: async (topic, shop, body) => {
    console.log(`App uninstalled from ${shop}`);
  },
};

app.post(
  "/webhooks",
  express.text({ type: "*/*" }),
  shopify.processWebhooks({ webhookHandlers })
);

// Ensure app is installed
app.use("/api/*", shopify.validateAuthenticatedSession());

// API routes
app.get("/api/products", async (req, res) => {
  const session = res.locals.shopify.session;
  const client = new shopify.api.clients.Graphql({ session });
  
  const response = await client.request(
    `query { products(first: 10) { edges { node { id title } } } }`
  );
  
  const data = await response.json();
  res.json(data.data.products);
});

// Frontend
app.get("/", shopify.ensureInstalledOnShop(), (req, res) => {
  res.send("<h1>My Shopify App</h1>");
});

app.listen(3000, () => {
  console.log("App running on port 3000");
});

Helper Functions

redirectOutOfApp()

Redirect users out of the Shopify Admin iframe:
app.get("/external", (req, res) => {
  shopify.redirectOutOfApp(req, res, "https://example.com");
});

Next Steps

Core API

Learn about @shopify/shopify-api

Session Storage

Explore session storage options

Express.js Docs

Express.js documentation

GitHub

View on GitHub

Build docs developers (and LLMs) love