Wire Hydrogen request handlers
A Shopify storefront answers more than page requests. Browser scripts call the Storefront API through the storefront's own origin, apps link to cart permalinks, customers follow checkout links, and old Liquid URLs need to redirect. Hydrogen's request handlers restore these routes in a headless storefront, so apps, analytics, and agents can integrate with your storefront the same way they integrate with a theme.
In this guide, you'll wire two functions around your framework's router: handleShopifyRoutes(), which answers Shopify-owned routes before your router runs, and handleShopifyRedirects(), which checks for Shopify redirects after your router returns a 404.
Anchor to How it worksHow it works
Hydrogen's request handlers are framework-neutral, but where they run depends on your framework. React Router follows this request flow:
- Your server creates a Shopify request context with
createShopifyRequestContext(). The Storefront API client and the request handlers share it for the rest of the request. handleShopifyRoutes()checks the request against the routes that Hydrogen owns. If the request matches, then it returns a response and your router never sees the request.- If nothing matches, then your framework routes and renders the request as usual.
- If your router returns a 404, then
handleShopifyRedirects()checks for a Shopify redirect for that URL. If one exists, then it returns the redirect. - If there's no redirect, then your framework renders its 404 page.
- Before your response leaves the server, call
requestContext.applyResponseHeaders(response.headers). Responses fromhandleShopifyRoutes()andhandleShopifyRedirects()already include them.
handleShopifyRedirects() can make a Storefront API request, so it runs only after a 404.
Anchor to Routes Hydrogen handlesRoutes Hydrogen handles
handleShopifyRoutes() answers the following routes:
| Route | Purpose |
|---|---|
/api/{version}/graphql.json | Proxies Storefront API requests from browser scripts through your origin. |
/__shopify/* | Proxies other Shopify APIs that browser modules call. |
/checkout | Redirects customers to checkout for the current cart, or to / without one. |
/cart/{variantId}:{quantity} | Cart permalinks that redirect to your store's domain (demostore.mock.shop for mock stores). |
/buy/{itemId}:{quantity} | Buy permalinks for agentic commerce. |
/cart.js, /cart/add.js, /cart/update.js, /cart/change.js, /cart/clear.js | The AJAX cart URLs that theme apps call. |
/api/mcp, /api/ucp/mcp | The storefront's MCP endpoints for agents. |
/account/login, /account/authorize, /account/logout, /account/refresh | Without a matching registered handler, GET and HEAD redirect to your store's domain, and other methods return 405. |
GET or HEAD /.well-known/ucp | Proxies your store's UCP discovery profile. |
/.well-known/* | Allowlisted resources, like the Apple Pay domain association file, proxied from your Online Store. |
/graphiql | A GraphiQL explorer. Development only. |
/products/{handle}?variant={id} | Redirects Liquid-style variant URLs to option parameters. |
| Registered handler groups | For example, /api/cart, /api/predictive-search, and /account/login. |
Checkout and cart permalink redirects outside / default to payment=shop_pay; an incoming payment parameter overrides it.
handleShopifyRedirects() handles the following requests after a 404:
/admin, which redirects to your store's Shopify admin.- Shopify's standard storefront paths, when route templates map them to your app's paths.
- URL redirects that the merchant configures in the Shopify admin.
- Same-origin redirects from query parameters.
Anchor to RequirementsRequirements
- Deploy a template, or add Hydrogen to an existing project.
- Create a Storefront API client in your server request lifecycle.
- Create a route templates manifest. It can be empty.
- For React Router 7, set
future.v8_middleware: trueinreact-router.config.ts. React Router 8 turns on middleware by default.
Anchor to Step 1: Create a session managerStep 1: Create a session manager
handleShopifyRoutes() takes a session manager, which stores session values such as customer account tokens. Until you add customer accounts, you can use a session manager that keeps values in memory for the length of one request:
In-memory session manager
lib/session.tsWhen you add customer accounts, replace it with a cookie-backed session manager.
Anchor to Step 2: Run Shopify routes before your routerStep 2: Run Shopify routes before your router
Create a request handler that runs before your framework's router. It creates the Storefront API client and session manager, and passes the same objects to handleShopifyRoutes() that the rest of the request uses. It also checks for Shopify redirects after a 404 and applies response headers, which the next steps describe. The createRequestStorefrontClient() helper comes from Storefront API client.
Don't await handleShopifyRoutes() right away. It returns null synchronously when no route matches, and a promise of a Response when one does. Check the result first, and then return it.
Shopify request handler
lib/shopify.tsCall handleRequest() from your app's request entry point, the first server code that runs for each request. Pass a next function that runs your framework's router and returns its response. In next, store the Storefront API client in request-scoped context, so your loaders reuse it instead of creating a second client. In Nuxt, run handleShopifyRoutes() in server middleware, handleShopifyRedirects() in error.vue after a 404, and applyResponseHeaders() in Nitro's beforeResponse hook. For the React Router and Next.js code, refer to Framework wiring.
If your server wraps requests in a try/catch that converts errors into a Response, then use return await shopifyResponse inside that block, so that a rejected route reaches your error handling.
In Next.js, the entry point is proxy.ts, which replaces middleware.ts in Next.js 16. The proxy can't pass the client to Server Components, so its next function forwards the request headers instead, including the original URL in the x-storefront-url header. Keep the proxy's config.matcher broad, so that it reaches /admin, /api/cart, and /api/{version}/graphql.json. Don't narrow it to a list of Hydrogen paths, because that list goes stale when Hydrogen adds routes.
Anchor to Step 3: Check for redirects after a 404Step 3: Check for redirects after a 404
handleRequest() calls handleShopifyRedirects() only after next() returns a 404. It takes the request, the Storefront API client, and your route templates. It doesn't take a session manager.
Add one more file, so that unknown paths reach this check:
React Router
Add a final splat route that returns a 404, so that unknown paths reach the root middleware as a 404 response:
Splat route
app/routes/catchall.tsxRegister the splat route last in app/routes.ts:
Register the splat route
app/routes.tsNext.js
proxy.ts can't inspect the routed response, because NextResponse.next() returns before the router runs. Run the check in app/not-found.tsx instead, using the URL that the proxy forwarded:
Check for Shopify redirects after a 404
app/not-found.tsxWith Cache Components, keep request-time work in an async child component under <Suspense>, and call connection() before headers(), as the code does.
The redirect() function doesn't keep Hydrogen's 301 status. If a permanent redirect status matters for your storefront, then return the Response from handleShopifyRedirects() directly from a route that can return one.
Add a final splat route that returns a 404, so that unknown paths reach the root middleware as a 404 response:
Splat route
app/routes/catchall.tsxRegister the splat route last in app/routes.ts:
Register the splat route
app/routes.tsAnchor to Step 4: Register handler groupsStep 4: Register handler groups
Hydrogen features that need their own server routes come as handler groups. Create each group once at module scope, and then pass it to handleShopifyRoutes() in the handlers array of handleRequest():
The following handler groups are available:
| Handler group | Created by | Routes | Guide |
|---|---|---|---|
| Cart | createCartServerHandlers() | /api/cart | Build cart UI |
| Predictive search | createPredictiveSearchServerHandlers() | /api/predictive-search | Add predictive search |
| Customer accounts | createCustomerAccountServerHandlers() from @shopify/hydrogen/customer-account | /account/login, /account/authorize, /account/refresh, POST /account/logout | Customer accounts |
Because handleShopifyRoutes() answers these routes, your app doesn't need route files for them.
Link to the customer account routes with plain <a> and <form> elements, not your framework's client-side navigation components such as next/link or React Router's <Link> and <Form>.
Anchor to Step 5: Apply response headersStep 5: Apply response headers
Captured Shopify cookies reach non-HTML responses to non-document requests other than GET or HEAD when the request has an essential cookie, is a consent request, or explicitly establishes a session. Hydrogen computes cache-safety headers locally.
- Responses that
handleShopifyRoutes()andhandleShopifyRedirects()return already include these headers. Return them as they are. - For every other response, call
requestContext.applyResponseHeaders(response.headers)at the final response boundary, as the code in step 2 does.
If your app commits session cookies, then append the session headers to the response in handleRequest() first, and then call applyResponseHeaders():
Wire request handlers into your production server code, not a development-only hook. Hydrogen limits only /graphiql to development.
Anchor to Step 6: Verify your wiringStep 6: Verify your wiring
Start your development server, and then send the following requests. Replace <port> with your server's port and {version} with the Storefront API version that your client uses.
-
Query the Storefront API proxy:
Terminal
curl -i -X POST http://localhost:<port>/api/{version}/graphql.json \-H "content-type: application/json" \-d '{"query":"{ shop { name } }"}'The response is Storefront API JSON, not your framework's 404 page. The proxy forwards the
X-Shopify-Storefront-Access-Tokenheader from the request when one is present. It doesn't add a token from your server config. -
Request
/admin:Terminal
curl -i http://localhost:<port>/adminThe response redirects to your store's Shopify admin.
-
Request a path that doesn't exist:
Terminal
curl -i http://localhost:<port>/this-does-not-existThe response is your framework's 404 page, unless the store has a URL redirect for that path.
-
If you registered the cart handlers, then request
/api/cart:Terminal
curl -i http://localhost:<port>/api/cartWithout a cart cookie, the response body is
{"cart":null}. -
Request a product URL with a Liquid-style variant ID, such as
/products/{handle}?variant={numeric_id}. Use your app's product path if your route templates change it. The response is a302redirect to the same product with option parameters. An invalid ID, such as?variant=garbage, renders the product page as usual. -
If your route templates include
productInCollection, then request a collection-scoped product URL:Terminal
curl -i http://localhost:<port>/collections/{collection_handle}/products/{product_handle}The response redirects after a router 404 to the path in your template.
-
If your route templates include a custom
productpath, then request/products/{handle}. The response redirects after a router 404 to your product path.
For the full list of checks, refer to the production checklist.
Anchor to Framework wiringFramework wiring
Hydrogen is framework-agnostic. This section shows where its pieces go in a React Router app and a Next.js App Router app, using code trimmed from the React Router and Next.js templates. For other frameworks, refer to Other frameworks.
React Router
The request entry point is a middleware export on the root route, which runs for every request. In React Router 7, turn on middleware with the future.v8_middleware flag. React Router 8 turns it on by default.
Turn on middleware
react-router.config.tsCall handleRequest() from the root middleware. In next, store the Storefront API client in router context, and then call the router's next():
Request entry point
app/lib/context.ts
Router context
import type {StorefrontClient} from '@shopify/hydrogen';
import {createContext} from 'react-router';
export const storefrontContext = createContext<StorefrontClient>();Root route middleware
import type {Route} from './+types/root';
import {storefrontContext} from './lib/context';
import {handleRequest} from './lib/shopify';
export const middleware: Route.MiddlewareFunction[] = [
({request, context}, next) =>
handleRequest(request, async (_request, storefrontClient) => {
context.set(storefrontContext, storefrontClient);
return next();
}),
];Each route's loader reads the client from router context and runs on the server. Throw a 404 Response when the resource doesn't exist. Loaders from other guides, like loadProduct(), go in the same place.
Page data loader
app/routes/product.tsxIn the same app/root.tsx, the root route's loader loads the cart and layout data with the request's client. It reads your environment variables from envContext, which the template's Oxygen Worker sets in server.ts. For that file, refer to Create the Worker entry point. On another runtime, set envContext in your server entry before the router runs. Layout renders ShopifyScripts in <head> with useNavigate() as navigate, and App hands the cart to the cart provider:
Root layout
app/root.tsxReact examples in these docs are client components. React Router route modules render on the server and hydrate in the browser, so the examples work in them as they are. Leave out 'use client'.
Next.js
If you started from Vercel Shop, then your project has the same pieces in different files. To find each one, refer to Explore your storefront.
The request entry point is proxy.ts at the root of your project. Next.js 16 renamed middleware.ts to proxy.ts, and the exported middleware function to proxy. Call handleRequest() from it. In next, forward the request headers that Hydrogen adds, because the proxy can't pass objects to Server Components. Export a config.matcher that excludes static Next.js assets but keeps every app path.
Request entry point
proxy.tsServer Components read the forwarded headers with headers(). Create the request-scoped client from them inside React's cache(), so every component in one render shares the same client:
Request-scoped client
lib/storefront.tsUse getStorefrontClient() for the cart and anything personalized. Catalog pages use the module-scoped catalog client instead, so they don't read request headers and can be cached.
A page loads its own data as an async Server Component. params and searchParams are promises. Call notFound() when the resource doesn't exist. Loaders from other guides, like loadProduct(), go in the same place.
Page data loader
app/products/[handle]/page.tsxThe root layout renders ShopifyScripts in <head>, and loads the cart in an async child component under <Suspense>. The child calls connection() and passes the cart promise to a client provider without awaiting it, so the static shell doesn't wait for the cart:
Root layout
app/layout.tsx
Root layout
import {Suspense} from 'react';
import {ShopifyScriptsWithNavigation} from '@/components/ShopifyScriptsWithNavigation';
import {getAnalyticsShop} from '@/lib/analytics-shop';
import {shop} from '@/lib/config';
import {AppShell} from './app-shell';
export default async function RootLayout({children}: {children: React.ReactNode}) {
const {currency} = await getAnalyticsShop();
return (
<html lang="en">
<head>
<ShopifyScriptsWithNavigation shop={shop} currency={currency} />
</head>
<body>
<Suspense fallback={null}>
<AppShell>{children}</AppShell>
</Suspense>
</body>
</html>
);
}Cart loader
import {connection} from 'next/server';
import {cartHandlers} from '@/lib/cart-handlers';
import {getStorefrontClient} from '@/lib/storefront';
import {Providers} from './providers';
export async function AppShell({children}: {children: React.ReactNode}) {
await connection();
// Don't await the cart, so the shell can stream.
const cartData = getStorefrontClient().then((storefrontClient) =>
cartHandlers.get({storefrontClient}).then((result) => result.data),
);
return <Providers cartData={cartData}>{children}</Providers>;
}React examples in these docs are client components. Add 'use client' at the top of each component file that uses hooks, state, or browser APIs. Server Components render them and pass serializable props, including promises. ShopifyScripts needs the router's navigate function, so it lives in a client component too:
Client components
app/providers.tsx
Cart provider
'use client';
import type {ReactNode} from 'react';
import {CartProvider} from '@/lib/cart';
import type {cartHandlers} from '@/lib/cart-handlers';
export type CartData = Awaited<ReturnType<typeof cartHandlers.get>>['data'];
export function Providers({
cartData,
children,
}: {
cartData?: Promise<CartData>;
children: ReactNode;
}) {
return <CartProvider initialData={cartData}>{children}</CartProvider>;
}Shopify scripts
'use client';
import {ShopifyScripts, type ShopifyScriptsProps} from '@shopify/hydrogen/react';
import {useRouter} from 'next/navigation';
import {analyticsConsent, defaultI18n} from '@/lib/config';
import {routeTemplates} from '@/lib/route-templates';
type ShopConfig = NonNullable<ShopifyScriptsProps['shop']>;
export function ShopifyScriptsWithNavigation({
shop,
currency,
}: {
shop: ShopConfig;
currency: string;
}) {
const router = useRouter();
return (
<ShopifyScripts
i18n={{...defaultI18n, currency}}
shop={shop}
consent={analyticsConsent}
navigate={(url: string) => router.push(url)}
routes={routeTemplates}
/>
);
}The request entry point is a middleware export on the root route, which runs for every request. In React Router 7, turn on middleware with the future.v8_middleware flag. React Router 8 turns it on by default.
Turn on middleware
react-router.config.tsCall handleRequest() from the root middleware. In next, store the Storefront API client in router context, and then call the router's next():
Request entry point
app/lib/context.ts
Router context
import type {StorefrontClient} from '@shopify/hydrogen';
import {createContext} from 'react-router';
export const storefrontContext = createContext<StorefrontClient>();Root route middleware
import type {Route} from './+types/root';
import {storefrontContext} from './lib/context';
import {handleRequest} from './lib/shopify';
export const middleware: Route.MiddlewareFunction[] = [
({request, context}, next) =>
handleRequest(request, async (_request, storefrontClient) => {
context.set(storefrontContext, storefrontClient);
return next();
}),
];Each route's loader reads the client from router context and runs on the server. Throw a 404 Response when the resource doesn't exist. Loaders from other guides, like loadProduct(), go in the same place.
Page data loader
app/routes/product.tsxIn the same app/root.tsx, the root route's loader loads the cart and layout data with the request's client. It reads your environment variables from envContext, which the template's Oxygen Worker sets in server.ts. For that file, refer to Create the Worker entry point. On another runtime, set envContext in your server entry before the router runs. Layout renders ShopifyScripts in <head> with useNavigate() as navigate, and App hands the cart to the cart provider:
Root layout
app/root.tsxReact examples in these docs are client components. React Router route modules render on the server and hydrate in the browser, so the examples work in them as they are. Leave out 'use client'.
Anchor to Other frameworksOther frameworks
The steps on this page work in any framework that gives your server code the incoming Request. The following table shows where each piece goes in frameworks that don't have a template. Each framework links to a development example in the Hydrogen repository that shows the full wiring. The examples show how the pieces fit together, but they aren't starter templates.
| Framework | Run Shopify routes | Store the client for loaders | Check for redirects after a 404 | Apply response headers |
|---|---|---|---|---|
| Nuxt | Server middleware in server/middleware/ | event.context | error.vue, when the status is 404 | A Nitro plugin that hooks beforeResponse |
| SvelteKit | The handle hook in src/hooks.server.ts | event.locals | The handle hook, after resolve() returns | The handle hook |
| Astro | onRequest in src/middleware.ts | locals | onRequest, after next() returns | onRequest |
| SolidStart | onRequest middleware in src/middleware.ts | event.locals | A catch-all route, src/routes/[...404].tsx | onBeforeResponse middleware |
In Nuxt, server middleware receives an H3 event instead of a Request. Convert it with toWebRequest(event) before you pass it to Hydrogen, and return a matched Shopify route with sendWebResponse(event, response).
To load Shopify's scripts, call initializeShopifyScripts() from @shopify/hydrogen once in your root layout after it mounts. Pass your route templates, and your router's navigate function if it has one.