Shopify Development (2026)
Comprehensive guide for building Shopify apps, extensions, Hydrogen storefronts, and integrations using current best practices.
DevMCP Server
Shopify provides an official MCP server for AI-assisted development. If not already configured:
# Claude Code
claude mcp add --transport stdio shopify-dev-mcp -- npx -y @shopify/dev-mcp@latest
The DevMCP server provides tools to search Shopify docs, explore API schemas (Admin GraphQL, Storefront, Functions, Liquid, Polaris, POS UI Extensions), and get up-to-date answers. Use it for API field lookups and schema exploration.
Stack & Tooling
| Layer | Current (2026) |
|---|---|
| App framework | React Router (formerly Remix) via @shopify/shopify-app-react-router |
| Scaffold | shopify app init → React Router template |
| CLI | @shopify/cli — init, dev, deploy, generate extension |
| UI (embedded apps) | Polaris Web Components (not Polaris React — deprecated) |
| UI (extensions) | Polaris Web Components + Preact (not React) — 64KB bundle limit |
| App Bridge | CDN v4 — https://cdn.shopify.com/shopifycloud/app-bridge.js |
| APIs | Admin GraphQL (primary), Storefront GraphQL, Functions API |
| Custom data | Metafields + Metaobjects via TOML or GraphQL |
| Headless | Hydrogen + Oxygen (React Router v7, Vite, edge rendering) |
| Functions | Wasm — Rust (recommended) or JavaScript (Javy) |
| Config | shopify.app.toml — single source of truth |
Critical:
- REST Admin API is legacy since October 1, 2024. As of April 1, 2025, all new public apps must use GraphQL Admin API exclusively.
- Shopify Scripts: no editing/publishing after April 15, 2026; all scripts stop executing June 30, 2026. Migrate to Functions now.
- Polaris React (
@shopify/polaris) is deprecated (maintenance mode only). Use Polaris Web Components. - UI Extensions (checkout, admin, POS) must use Preact (not React) for API 2025-10+. 64KB bundle size limit enforced at deploy.
Scaffolding
shopify app init # Interactive — choose React Router template
shopify app dev # Local dev with tunnel + hot reload
shopify app generate extension # Add extensions (checkout UI, functions, theme, admin, POS)
shopify app deploy # Deploy app version (config + all extensions)
Directory Structure (React Router template)
my-app/
├── shopify.app.toml # App config, scopes, webhooks, metaobjects
├── app/
│ ├── entry.server.tsx # Server entry
│ ├── root.tsx # Root layout
│ ├── routes/
│ │ ├── app.tsx # Authenticated layout (App Bridge + Polaris)
│ │ ├── app._index.tsx # App home page
│ │ └── webhooks.tsx # Webhook handler
│ └── shopify.server.ts # shopifyApp() config — auth, session storage
├── extensions/ # All extensions live here
│ ├── checkout-ui/
│ ├── theme-extension/
│ └── my-function/
├── prisma/
│ └── schema.prisma # Session table (required)
└── package.json
Authentication & Sessions
- App Bridge v4 (CDN) automatically handles session tokens — no manual JWT fetch needed. The manual setup guide is for legacy App Bridge 2.0 only.
- Embedded apps use session tokens (JWT, 1-minute lifetime). Third-party cookies are blocked by browsers.
@shopify/shopify-app-react-router(recommended) or@shopify/shopify-app-remix(v4, still works) handle OAuth + token exchange automatically.- Session storage: Prisma adapter (
@shopify/shopify-app-session-storage-prisma) — the Session table is required. - Use
authenticate.admin(request)in loaders/actions for authenticated Admin API access. - Use
authenticate.public.checkout(request)for checkout extension requests. - Never expose Admin API tokens in client-side code.
- Use offline access tokens for bulk operations and webhooks (online tokens expire in 24h).
// app/shopify.server.ts
import { shopifyApp } from "@shopify/shopify-app-react-router";
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
import prisma from "./db.server";
const shopify = shopifyApp({
sessionStorage: new PrismaSessionStorage(prisma),
// API key, secret, scopes, and app URL are read from
// shopify.app.toml + environment variables automatically
});
export default shopify;
export const authenticate = shopify.authenticate;
// In a route loader:
export async function loader({ request }: LoaderFunctionArgs) {
const { admin } = await authenticate.admin(request);
const response = await admin.graphql(`{ shop { name } }`);
const { data } = await response.json();
return { shopName: data.shop.name };
}
Migration note: If on
@shopify/shopify-app-remix, migrate to@shopify/shopify-app-react-router— see migration guide.
Polaris Web Components (NOT Polaris React)
Polaris React is deprecated. Use Polaris Web Components — framework-agnostic custom HTML elements.
Setup
<head>
<meta name="shopify-api-key" content="%SHOPIFY_API_KEY%" />
<script src="https://cdn.shopify.com/shopifycloud/app-bridge.js"></script>
<script src="https://cdn.shopify.com/shopifycloud/polaris.js"></script>
</head>
Usage in React (App Home)
function ProductForm() {
const [name, setName] = useState("");
return (
<s-page title="New Product">
<s-layout>
<s-layout-section>
<s-card>
<s-form-layout>
<s-text-field
label="Product name"
value={name}
onInput={(e) => setName(e.currentTarget.value)}
required
/>
<s-button variant="primary" type="submit">Save</s-button>
</s-form-layout>
</s-card>
</s-layout-section>
</s-layout>
</s-page>
);
}
Key Patterns
- Elements are prefixed with
s-(e.g.,s-card,s-button,s-badge,s-data-table). - Use
onInputfor real-time updates,onChangefor on-blur/enter validation. - All form values are strings — parse numbers/booleans yourself.
toneapplies semantic color (critical,success,info);variantsets visual weight (primary,secondary).- Links with
target="auto"auto-detect internal (_self) vs external (_blank). - Do not apply custom CSS to Polaris components — they have built-in design system styling.
Navigation (App Bridge)
// Handle Shopify navigation events in Remix/React Router
useEffect(() => {
const handler = (event) => {
const href = event.target.getAttribute("href");
if (href) navigate(href);
};
document.addEventListener("shopify:navigate", handler);
return () => document.removeEventListener("shopify:navigate", handler);
}, [navigate]);
Use <s-navigation-menu> for sidebar nav. Use shopify.navigate() for programmatic navigation. Never use <a> tags or redirect from react-router in embedded apps — use Link from react-router or the redirect helper from authenticate.admin.
Admin GraphQL API
Query Patterns
// Simple query
const response = await admin.graphql(`
query {
products(first: 10) {
edges {
node { id title handle }
}
}
}
`);
// Mutation with variables
const response = await admin.graphql(`
mutation createProduct($input: ProductInput!) {
productCreate(input: $input) {
product { id title }
userErrors { field message }
}
}
`, { variables: { input: { title: "New Product" } } });
// Always check userErrors
const { data } = await response.json();
if (data.productCreate.userErrors.length > 0) {
throw new Error(data.productCreate.userErrors[0].message);
}
Rate Limiting
- Admin GraphQL uses calculated query cost (cost points, not request count).
- Complex queries with nested connections cost more points.
- Use
cursor-based pagination for large datasets. - For bulk data: use bulk operations (
bulkOperationRunQuery/bulkOperationRunMutation) — up to 5 concurrent queries and 5 concurrent mutations per shop in API 2026-01+ (previously 1 each). - Use offline access tokens for bulk operations (online tokens expire before completion).
API Version
Always pin to a stable version (e.g., 2026-01). Set in shopify.app.toml:
[webhooks]
api_version = "2026-01"
Storefront API
- For all customer-facing data: products, collections, cart, checkout.
- No rate limits on request count — scales for buyer traffic surges.
- Requires Storefront access token (public, safe for client-side).
- Use in Hydrogen storefronts, mobile apps, or headless frontends.
Metafields & Metaobjects
When to Use Which
| Use Case | Tool |
|---|---|
| Add custom field to existing resource (product, order, customer) | Metafield |
| Create standalone reusable structured data | Metaobject |
| App-internal data model | Metaobject with $app: prefix in TOML |
App-Owned Metafields (TOML)
# shopify.app.toml — auto-created on `shopify app dev`
[[metaobjects]]
type = "$app:qrcode"
[metaobjects.access]
admin = "merchant_read_write"
storefront = "public_read"
[[metaobjects.fields]]
key = "title"
type = "single_line_text_field"
name = "Title"
required = true
[[metaobjects.fields]]
key = "product"
type = "product_reference"
name = "Product"
Merchant-Owned Metafields (GraphQL)
mutation {
metafieldDefinitionCreate(definition: {
name: "Warranty Info"
namespace: "custom" # NOT $app — merchant-owned
key: "warranty_info"
type: "multi_line_text_field"
ownerType: PRODUCT
access: { storefront: PUBLIC_READ }
}) {
createdDefinition { name namespace key }
}
}
$appnamespace = app-owned (read-only in admin by default).customor other namespaces = merchant-owned.- Use TOML for app-owned; GraphQL for merchant-owned.
Webhooks
Configuration (TOML)
[webhooks]
api_version = "2026-01"
[[webhooks.subscriptions]]
topics = ["orders/create", "products/update"]
uri = "/webhooks"
[[webhooks.subscriptions]]
compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
uri = "https://app.example.com/webhooks"
Mandatory Compliance (Required for App Store)
Every public app must handle these three webhooks:
customers/data_request— respond with customer data you storecustomers/redact— delete customer personal datashop/redact— delete all shop data after uninstall
Validate HMAC on every webhook. Return 401 for invalid HMAC, 200 for valid.
GraphQL Subscriptions
mutation {
webhookSubscriptionCreate(
topic: ORDERS_CREATE
webhookSubscription: {
callbackUrl: "https://app.example.com/webhooks"
format: JSON
}
) {
webhookSubscription { id }
userErrors { field message }
}
}
Shopify Functions (Wasm)
Extend Shopify backend logic (discounts, validation, delivery, payment customization) as sandboxed WebAssembly modules.
Language Choice
- Rust: 3x faster than JS. Required for public apps with complex logic or high line-item counts. Stays within instruction count limits.
- JavaScript (via Javy → Wasm): Viable for prototyping and simple logic (<5ms realistic cases). Lower barrier to entry.
Execution Order in Checkout
- Cart Transform → 2. Discount (cart lines) → 3. Fulfillment/Order Routing → 4. Delivery Customization → 5. Discount (delivery) → 6. Payment Customization → 7. Cart Validation
Scaffold & Build
shopify app generate extension # Choose function type
shopify app function schema # Regenerate GraphQL schema
shopify app function run # Test locally
Structure
extensions/my-discount/
├── shopify.extension.toml # Extension config (target, API version)
├── src/
│ ├── run.graphql # Input query — what data the function receives
│ └── run.rs (or run.js) # Function logic
├── schema.graphql # Generated API schema
└── Cargo.toml (or package.json)
Performance: Functions run during checkout — delays block purchases. Minimize instruction count. Avoid unnecessary data in run.graphql.
Extensions
Types
| Extension | Purpose |
|---|---|
| theme_app_extension | Liquid blocks/snippets in Online Store themes |
| checkout_ui_extension | Custom UI at checkout extension points |
| admin_action | Dropdown actions on admin resource pages |
| admin_block | Embedded blocks on admin resource pages |
| pos_ui_extension | POS interface extensions |
| Functions | Backend logic (discounts, delivery, validation, payments) |
Theme App Extensions
Required for all new App Store apps (replaces manual theme code editing).
extensions/theme-extension/
├── shopify.extension.toml
├── blocks/
│ └── rating.liquid
├── snippets/
├── assets/
└── locales/
Checkout UI Extensions
20+ extension points (e.g., Checkout::Dynamic::Render). Use Polaris Web Components. Keep lightweight — mobile-first.
Critical: React is deprecated for UI extensions as of API 2025-10. Must use Preact for any API version 2025-10+. Vanilla React + ui-react package exceeds the 64KB bundle limit.
# shopify.extension.toml
api_version = "2026-01"
[[extensions]]
name = "my-checkout-ext"
handle = "my-checkout-ext"
type = "ui_extension"
// package.json dependencies for UI extensions
{
"dependencies": {
"preact": "^10.10.x",
"@preact/signals": "^2.3.x",
"@shopify/ui-extensions": "2026.1.x"
}
}
// tsconfig.json for Preact JSX
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}
Hydrogen & Oxygen (Headless)
Stack
- Hydrogen: React framework (React Router v7 + Vite) with Storefront API utilities, streaming SSR, granular caching.
- Oxygen: Shopify's edge runtime (based on Cloudflare workerd). Multiple preview environments. CI/CD via GitHub.
- Vite-first: Legacy Remix compiler no longer supported — must use Vite.
Key Patterns
// Caching — be explicit
const { storefront } = await context.storefront.query(PRODUCT_QUERY, {
cache: CacheLong(), // or CacheShort(), CacheNone(), CacheCustom()
});
// Streaming for performance
export default function Product() {
return (
<Suspense fallback={<ProductSkeleton />}>
<Await resolve={recommendedProducts}>
{(products) => <ProductGrid products={products} />}
</Await>
</Suspense>
);
}
AI Integration (2026)
Hydrogen on Oxygen supports Storefront MCP — build AI agents into storefronts for personalized recommendations and checkout guidance using real-time Storefront API data.
App Store Submission Checklist
- GraphQL only — no REST API calls
- Session tokens — no cookie-based auth
- Mandatory compliance webhooks — all three implemented and HMAC-validated
- Theme app extensions — no manual theme code injection
- Polaris Web Components — consistent admin UI
- Performance — must not reduce Lighthouse score by >10 points
- Privacy — GDPR/CCPA compliant, data handling documented
- Credentials — provide valid test credentials with full feature access
- Screencast — complete walkthrough of setup + functionality
- No Shopify trademarks in app icon/banner/screenshots
- Review timeline: 7–14 days. Statuses: Draft → Submitted → Reviewed → Published.
Common Pitfalls
- Using
<a>or bareredirect()in embedded apps (breaks iframe). UseLinkfromreact-routeror the redirect helper fromauthenticate.admin. - Using Polaris React (
@shopify/polaris) instead of Polaris Web Components — deprecated, maintenance-only. - Using React in UI extensions targeting API 2025-10+ — must use Preact (64KB bundle limit).
- Exposing Admin API tokens client-side.
- Using REST API in new apps (legacy since Oct 2024; blocked for new public apps since April 2025).
- Not validating webhook HMAC.
- Using online access tokens for bulk operations (they expire in 24h).
- Heavy checkout extensions that slow mobile performance.
- Not handling
userErrorsin GraphQL mutations. - Missing the
$app:namespace prefix for app-owned metaobjects. - Forgetting to run
shopify app function schemaafter changing API version. - Using
@shopify/shopify-app-remixfor new projects instead of@shopify/shopify-app-react-router.