Shopify Apps
Expert patterns for Shopify app development including Remix/React Router apps, embedded apps with App Bridge, webhook handling, GraphQL Admin API, Polaris components, billing, and app extensions.
Patterns
React Router App Setup
Modern Shopify app template with React Router
When to use: Starting a new Shopify app
Template
Create new Shopify app with CLI
npm init @shopify/app@latest my-shopify-app
Project structure
my-shopify-app/
├── app/
│ ├── routes/
│ │ ├── app._index.tsx # Main app page
│ │ ├── app.tsx # App layout with providers
│ │ ├── auth.$.tsx # Auth callback
│ │ └── webhooks.tsx # Webhook handler
│ ├── shopify.server.ts # Server configuration
│ └── root.tsx # Root layout
├── extensions/ # App extensions
├── shopify.app.toml # App configuration
└── package.json
// shopify.app.toml name = "my-shopify-app" client_id = "your-client-id" application_url = "https://your-app.example.com"
[access_scopes] scopes = "read_products,write_products,read_orders"
[webhooks] api_version = "2024-10"
[webhooks.subscriptions] topics = ["orders/create", "products/update"] uri = "/webhooks"
[auth] redirect_urls = ["https://your-app.example.com/auth/callback"]
// app/shopify.server.ts import "@shopify/shopify-app-remix/adapters/node"; import { LATEST_API_VERSION, shopifyApp, DeliveryMethod, } from "@shopify/shopify-app-remix/server"; import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma"; import prisma from "./db.server";
const shopify = shopifyApp({ apiKey: process.env.SHOPIFY_API_KEY!, apiSecretKey: process.env.SHOPIFY_API_SECRET!, scopes: process.env.SCOPES?.split(","), appUrl: process.env.SHOPIFY_APP_URL!, authPathPrefix: "/auth", sessionStorage: new PrismaSessionStorage(prisma), distribution: AppDistribution.AppStore, future: { unstable_newEmbeddedAuthStrategy: true, }, ...(process.env.SHOP_CUSTOM_DOMAIN ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] } : {}), });
export default shopify; export const apiVersion = LATEST_API_VERSION; export const authenticate = shopify.authenticate; export const sessionStorage = shopify.sessionStorage;
Notes
- React Router replaced Remix as recommended template (late 2024)
- unstable_newEmbeddedAuthStrategy enabled by default for new apps
- Webhooks configured in shopify.app.toml, not code
- Run 'shopify app deploy' to apply configuration changes
Embedded App with App Bridge
Render app embedded in Shopify Admin
When to use: Building embedded admin app
Template
// app/routes/app.tsx - App layout with providers import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react"; import { AppProvider } from "@shopify/shopify-app-remix/react"; import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
export async function loader({ request }: LoaderFunctionArgs) { await authenticate.admin(request); return json({ apiKey: process.env.SHOPIFY_API_KEY! }); }
export default function App() { const { apiKey } = useLoaderData<typeof loader>();
return ( <AppProvider isEmbeddedApp apiKey={apiKey}> <ui-nav-menu> <Link to="/app" rel="home">Home</Link> <Link to="/app/products">Products</Link> <Link to="/app/settings">Settings</Link> </ui-nav-menu> <Outlet /> </AppProvider> ); }
export function ErrorBoundary() { const error = useRouteError(); return ( <AppProvider isEmbeddedApp> <Page> <Card> <Text as="p" variant="bodyMd"> Something went wrong. Please try again. </Text> </Card> </Page> </AppProvider> ); }
// app/routes/app._index.tsx - Main app page import { Page, Layout, Card, Text, BlockStack, Button, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react";
export async function loader({ request }: LoaderFunctionArgs) { const { admin } = await authenticate.admin(request);
// GraphQL query
const response = await admin.graphql( query { shop { name email } } );
const { data } = await response.json(); return json({ shop: data.shop }); }
export default function Index() { const { shop } = useLoaderData<typeof loader>();
return ( <Page> <TitleBar title="My Shopify App" /> <Layout> <Layout.Section> <Card> <BlockStack gap="200"> <Text as="h2" variant="headingMd"> Welcome to {shop.name}! </Text> <Text as="p" variant="bodyMd"> Your app is now connected to this store. </Text> <Button variant="primary"> Get Started </Button> </BlockStack> </Card> </Layout.Section> </Layout> </Page> ); }
Notes
- App Bridge required for Built for Shopify (July 2025)
- Polaris components match Shopify Admin design
- TitleBar and navigation from App Bridge
- Always authenticate requests with authenticate.admin()
Webhook Handling
Secure webhook processing with HMAC verification
When to use: Receiving Shopify webhooks
Template
// app/routes/webhooks.tsx import type { ActionFunctionArgs } from "@remix-run/node"; import { authenticate } from "../shopify.server"; import db from "../db.server";
export const action = async ({ request }: ActionFunctionArgs) => { // Authenticate webhook (verifies HMAC signature) const { topic, shop, payload, admin } = await authenticate.webhook(request);
console.log(Received ${topic} webhook for ${shop});
// Process based on topic switch (topic) { case "ORDERS_CREATE": // Queue for async processing await queueOrderProcessing(payload); break;
case "PRODUCTS_UPDATE":
await handleProductUpdate(shop, payload);
break;
case "APP_UNINSTALLED":
// Clean up shop data
await db.session.deleteMany({ where: { shop } });
await db.shopData.delete({ where: { shop } });
break;
case "CUSTOMERS_DATA_REQUEST":
case "CUSTOMERS_REDACT":
case "SHOP_REDACT":
// GDPR webhooks - mandatory
await handleGDPRWebhook(topic, payload);
break;
default:
console.log(`Unhandled webhook topic: ${topic}`);
}
// CRITICAL: Return 200 immediately // Shopify expects response within 5 seconds return new Response(null, { status: 200 }); };
// Process asynchronously after responding async function queueOrderProcessing(payload: any) { // Use a job queue (BullMQ, etc.) await jobQueue.add("process-order", { orderId: payload.id, orderData: payload, }); }
async function handleProductUpdate(shop: string, payload: any) { // Quick sync operation only await db.product.upsert({ where: { shopifyId: payload.id }, update: { title: payload.title, updatedAt: new Date(), }, create: { shopifyId: payload.id, shop, title: payload.title, }, }); }
async function handleGDPRWebhook(topic: string, payload: any) { // GDPR compliance - required for all apps switch (topic) { case "CUSTOMERS_DATA_REQUEST": // Return customer data within 30 days break; case "CUSTOMERS_REDACT": // Delete customer data break; case "SHOP_REDACT": // Delete all shop data (48 hours after uninstall) break; } }
Notes
- Respond within 5 seconds or webhook fails
- Use job queues for heavy processing
- GDPR webhooks are mandatory for App Store
- HMAC verification handled by authenticate.webhook()
GraphQL Admin API
Query and mutate shop data with GraphQL
When to use: Interacting with Shopify Admin API
Template
// GraphQL queri