| name | shopify |
| description | Shopify app and storefront development. Use when writing, reviewing, or scaffolding Shopify apps (Remix/React Router), Hydrogen storefronts, theme app extensions, checkout UI extensions, Shopify Functions, or any code that touches Shopify APIs (Admin GraphQL, Storefront, App Bridge, Polaris). Triggers on shopify.app.toml, @shopify/* imports, Polaris components, Shopify CLI commands, Hydrogen/Oxygen, metafields/metaobjects, webhooks, or any Shopify development task. |
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 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
shopify app dev
shopify app generate extension
shopify app deploy
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).
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),
});
export default shopify;
export const authenticate = shopify.authenticate;
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
onInput for real-time updates, onChange for on-blur/enter validation.
- All form values are strings — parse numbers/booleans yourself.
tone applies semantic color (critical, success, info); variant sets 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)
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
const response = await admin.graphql(`
query {
products(first: 10) {
edges {
node { id title handle }
}
}
}
`);
const response = await admin.graphql(`
mutation createProduct($input: ProductInput!) {
productCreate(input: $input) {
product { id title }
userErrors { field message }
}
}
`, { variables: { input: { title: "New Product" } } });
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)
[[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"
key: "warranty_info"
type: "multi_line_text_field"
ownerType: PRODUCT
access: { storefront: PUBLIC_READ }
}) {
createdDefinition { name namespace key }
}
}
$app namespace = app-owned (read-only in admin by default).
custom or 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 store
customers/redact — delete customer personal data
shop/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
shopify app function schema
shopify app function run
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.
api_version = "2026-01"
[[extensions]]
name = "my-checkout-ext"
handle = "my-checkout-ext"
type = "ui_extension"
{
"dependencies": {
"preact": "^10.10.x",
"@preact/signals": "^2.3.x",
"@shopify/ui-extensions": "2026.1.x"
}
}
{
"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
const { storefront } = await context.storefront.query(PRODUCT_QUERY, {
cache: CacheLong(),
});
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 bare redirect() in embedded apps (breaks iframe). Use Link from react-router or the redirect helper from authenticate.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
userErrors in GraphQL mutations.
- Missing the
$app: namespace prefix for app-owned metaobjects.
- Forgetting to run
shopify app function schema after changing API version.
- Using
@shopify/shopify-app-remix for new projects instead of @shopify/shopify-app-react-router.