@shopify/shopify-app-react-router makes it easy to build Shopify apps using React Router v7. It provides the same powerful features as the Remix adapter with React Router’s routing model.
Installation
npm install @shopify/shopify-app-react-router @shopify/shopify-app-session-storage-prisma
Requires Node.js >= 20.0.0 and React Router >= 7.6.2
Quick Start
Create app/shopify.server.ts:
import { shopifyApp } from "@shopify/shopify-app-react-router/server" ;
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma" ;
import { restResources } from "@shopify/shopify-api/rest/admin/2024-01" ;
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 ! ,
apiVersion: "2024-01" ,
sessionStorage: new PrismaSessionStorage ( prisma ),
restResources ,
// Optional: configure webhooks
webhooks: {
APP_UNINSTALLED: {
deliveryMethod: "http" ,
callbackUrl: "/webhooks" ,
},
},
// Optional: lifecycle hooks
hooks: {
afterAuth : async ({ session }) => {
// Register webhooks after authentication
await shopify . registerWebhooks ({ session });
},
},
});
export default shopify ;
export const authenticate = shopify . authenticate ;
2. Add Authentication Routes
Create app/routes/auth.$.tsx:
import type { LoaderFunctionArgs } from "react-router" ;
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
await authenticate . admin ( request );
return null ;
};
3. Authenticate Requests
In your route loaders and actions:
import type { LoaderFunctionArgs } from "react-router" ;
import { data } from "react-router" ;
import { useLoaderData } from "react-router" ;
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin , session } = await authenticate . admin ( request );
const response = await admin . graphql (
`#graphql
query {
products(first: 10) {
edges {
node {
id
title
handle
}
}
}
}
`
);
const json = await response . json ();
return data ({ products: json . data . products . edges });
};
export default function ProductsPage () {
const { products } = useLoaderData < typeof loader >();
return (
< div >
{ products . map (({ node }) => (
< div key = {node. id } > {node. title } </ div >
))}
</ div >
);
}
Configuration
shopifyApp Options
Your app’s API key from the Partner Dashboard. Typically: process.env.SHOPIFY_API_KEY
Your app’s API secret from the Partner Dashboard. Typically: process.env.SHOPIFY_API_SECRET
OAuth scopes your app needs. Example: ["read_products", "write_orders"]
Your app’s URL (including protocol). Example: "https://myapp.example.com" or process.env.SHOPIFY_APP_URL
Shopify API version to use. Example: "2024-01"
distribution
AppDistribution
default: "AppDistribution.AppStore"
App distribution type:
AppDistribution.AppStore: Public app
AppDistribution.SingleMerchant: Private app
AppDistribution.ShopifyAdmin: Custom app in Shopify Admin
Whether to use online access tokens instead of offline. When true, both online and offline tokens are saved for background jobs.
Shop-specific webhook configuration. See Webhooks section.
Authentication
Admin Authentication
Authenticate admin requests in loaders and actions:
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin , session , billing , cors } = await authenticate . admin ( request );
// admin: GraphQL/REST client
// session: Current session
// billing: Billing utilities
// cors: CORS helper
return data ({ shop: session . shop });
};
API client for making Admin API requests. Methods:
admin.graphql(): Make GraphQL requests
admin.rest: Access REST API
Current authenticated session. Properties:
session.shop: Shop domain
session.accessToken: Access token
session.scope: Granted scopes
Billing utilities for the current shop. Methods:
billing.require(): Require active billing
billing.request(): Request new subscription
billing.check(): Check billing status
Helper to add CORS headers to responses.
GraphQL Requests
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin } = await authenticate . admin ( request );
const response = await admin . graphql (
`#graphql
query getProducts($first: Int!) {
products(first: $first) {
edges {
node {
id
title
handle
}
}
}
}
` ,
{
variables: { first: 10 },
}
);
const json = await response . json ();
return data ( json . data );
};
REST API Requests
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin } = await authenticate . admin ( request );
// Using REST client
const response = await admin . rest . get ({ path: "products" });
const products = response . body . products ;
// Using REST resources (type-safe)
const productsTyped = await admin . rest . resources . Product . all ({
session: admin . session ,
limit: 10 ,
});
return data ({ products });
};
Webhook Authentication
Handle incoming webhooks:
// app/routes/webhooks.tsx
import type { ActionFunctionArgs } from "react-router" ;
import { authenticate } from "~/shopify.server" ;
export const action = async ({ request } : ActionFunctionArgs ) => {
const { topic , admin , payload , session } = await authenticate . webhook ( request );
// Session may be undefined if app is uninstalled
if ( ! session ) {
throw new Response ();
}
switch ( topic ) {
case "PRODUCTS_UPDATE" :
await admin . graphql (
`#graphql
mutation setMetafield($productId: ID!, $time: String!) {
metafieldsSet(metafields: {
ownerId: $productId
namespace: "my-app",
key: "webhook_received_at",
value: $time,
type: "string",
}) {
metafields {
key
value
}
}
}
` ,
{
variables: {
productId: payload . admin_graphql_api_id ,
time: new Date (). toISOString (),
},
}
);
break ;
}
return new Response ();
};
POS Authentication
Authenticate Shopify POS requests:
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin , session } = await authenticate . pos ( request );
// Access POS-specific session data
return data ({ shop: session . shop });
};
Public Authentication
Handle public requests (App Proxy, Checkout UI Extensions):
// App Proxy
import { authenticate } from "~/shopify.server" ;
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { liquid , storefront } = await authenticate . public . appProxy ( request );
// Make Storefront API requests
const response = await storefront . graphql (
`query { shop { name } }`
);
// Return Liquid template
return liquid ( "<h1>{{ shop.name }}</h1>" );
};
Webhooks
Registering Webhooks
In Configuration
Manual Registration
import { DeliveryMethod } from "@shopify/shopify-app-react-router/server" ;
const shopify = shopifyApp ({
// ... other config
webhooks: {
PRODUCTS_CREATE: {
deliveryMethod: DeliveryMethod . Http ,
callbackUrl: "/webhooks" ,
},
ORDERS_PAID: {
deliveryMethod: DeliveryMethod . Http ,
callbackUrl: "/webhooks" ,
},
},
hooks: {
afterAuth : async ({ session }) => {
// Register webhooks after OAuth
await shopify . registerWebhooks ({ session });
},
},
});
import shopify from "~/shopify.server" ;
// In a loader or background job
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { session } = await authenticate . admin ( request );
// Register all configured webhooks
const response = await shopify . registerWebhooks ({ session });
return data ( response );
};
Billing
Handle app billing and subscriptions:
import { shopifyApp , BillingInterval } from "@shopify/shopify-app-react-router/server" ;
const shopify = shopifyApp ({
// ... other config
billing: {
"Premium Plan" : {
amount: 10.0 ,
currencyCode: "USD" ,
interval: BillingInterval . Every30Days ,
trialDays: 7 ,
},
},
});
Require Active Billing
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { billing } = await authenticate . admin ( request );
// Require active billing or redirect to payment
await billing . require ({
plans: [ "Premium Plan" ],
onFailure : async () => billing . request ({ plan: "Premium Plan" }),
});
// User has active subscription
return data ({ success: true });
};
Session Storage
Choose a session storage adapter:
Prisma
PostgreSQL
DynamoDB
npm install @shopify/shopify-app-session-storage-prisma
import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma" ;
import prisma from "~/db.server" ;
const shopify = shopifyApp ({
sessionStorage: new PrismaSessionStorage ( prisma ),
});
npm install @shopify/shopify-app-session-storage-postgresql
import { PostgreSQLSessionStorage } from "@shopify/shopify-app-session-storage-postgresql" ;
const shopify = shopifyApp ({
sessionStorage: new PostgreSQLSessionStorage (
"postgresql://user:pass@localhost/db"
),
});
npm install @shopify/shopify-app-session-storage-dynamodb
import { DynamoDBSessionStorage } from "@shopify/shopify-app-session-storage-dynamodb" ;
const shopify = shopifyApp ({
sessionStorage: new DynamoDBSessionStorage ({
tableName: "shopify-sessions" ,
config: { region: "us-east-1" },
}),
});
Lifecycle Hooks
const shopify = shopifyApp ({
// ... other config
hooks: {
afterAuth : async ({ session , admin }) => {
// Called after successful OAuth
console . log ( `Shop ${ session . shop } installed the app` );
// Register webhooks
await shopify . registerWebhooks ({ session });
// Create initial data
await admin . graphql (
`mutation { /* setup query */ }`
);
},
},
});
Error Handling
Handle authentication errors:
import { boundary } from "@shopify/shopify-app-react-router/server" ;
// Error boundary for authentication errors
export const ErrorBoundary = boundary . error ;
React Router v7 Features
Type-Safe Routes
React Router v7 provides type-safe routing:
import type { Route } from "./+types/products" ;
export const loader = async ({ request } : Route . LoaderArgs ) => {
const { admin } = await authenticate . admin ( request );
// Fully typed loader
};
export default function Products ({ loaderData } : Route . ComponentProps ) {
// Fully typed component props
return < div >{loaderData. products } </ div > ;
}
Server-Side Rendering
React Router v7 supports SSR out of the box:
export const loader = async ({ request } : LoaderFunctionArgs ) => {
const { admin } = await authenticate . admin ( request );
const response = await admin . graphql ( `query { shop { name } }` );
const json = await response . json ();
// Data is available during SSR
return data ({ shopName: json . data . shop . name });
};
Key Differences from Remix
The React Router adapter uses token exchange authentication by default, providing a more modern authentication experience.
Token Exchange Uses Shopify’s token exchange for authentication (no authorization code flow)
POS Support Built-in support for Shopify POS authentication
React Router v7 Leverages React Router v7’s type-safe routing
Same API Otherwise identical API to Remix adapter
Always use authenticate.admin() in routes that require authentication to ensure proper session validation.
Next Steps
Core API Learn about @shopify/shopify-api
Remix Adapter Compare with Remix adapter
React Router Docs React Router documentation