--- title: Express server in Hydrogen description: Deploy Hydrogen on Node.js with Express instead of Shopify Oxygen source_url: html: 'https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express' md: 'https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md' --- ExpandOn this page * [Requirements](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#requirements) * [Ingredients](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#ingredients) * [Step 1: Disable customer account API](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-1-disable-customer-account-api) * [Step 2: Update README for Express deployment](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-2-update-readme-for-express-deployment) * [Step 3: Add environment type definitions](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-3-add-environment-type-definitions) * [Step 4: Set up client-side hydration](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-4-set-up-client-side-hydration) * [Step 5: Add the Express template favicon](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-5-add-the-express-template-favicon) * [Step 6: Configure server-side rendering](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-6-configure-server-side-rendering) * [Step 7: Set up the development server](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-7-set-up-the-development-server) * [Step 8: Simplify the root layout](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-8-simplify-the-root-layout) * [Step 9: Create the Express server](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-9-create-the-express-server) * [Step 10: Configure routes for Express](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-10-configure-routes-for-express) * [Step 11: Create a basic homepage](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-11-create-a-basic-homepage) * [Step 12: Add a minimal product page](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-12-add-a-minimal-product-page) * [Step 13: Add basic styles](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-13-add-basic-styles) * [Step 14: Update ESLint configuration](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-14-update-eslint-configuration) * [Step 15: Install Express dependencies](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-15-install-express-dependencies) * [Step 16: Configure Vite for Node.​js](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-16-configure-vite-for-nodejs) * [Deleted Files](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#deleted-files) * [Next steps](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#next-steps) # Express server in Hydrogen This recipe transforms a Hydrogen skeleton template to run on a standard Node.js Express server, making it deployable to any Node.js hosting platform instead of Shopify Oxygen. It maintains core Hydrogen functionality including GraphQL codegen and Storefront API integration while replacing Oxygen-specific features with Express equivalents. Key changes: * Replaces Oxygen server with Express server * Uses Vite for development with hot module replacement * Implements session management through Express middleware * Provides production-ready server configuration * Keeps GraphQL codegen functionality intact Technical details: * Uses nodemon for development server with automatic restarts * Environment variables are loaded from .env file using dotenv * Session management is handled through Express middleware with SESSION\_SECRET * GraphQL codegen still works with Storefront API types * Compatible with React Router 7.8.x * The .graphqlrc.ts file is preserved with customer account section commented out *** ## Requirements * Node.js 20 or higher (less than 22.0.0) for production deployment * npm or yarn package manager * Shopify Storefront API credentials *** ## Ingredients *New files added to the template by this recipe.* | File | Description | | - | - | | [app/env.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/app/env.ts) | Environment type definitions for Express server | | [public/favicon.svg](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/public/favicon.svg) | Favicon for Express template | | [scripts/dev.mjs](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/scripts/dev.mjs) | Development orchestration script | | [server.mjs](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/server.mjs) | Express server with Hydrogen context and SSR | *** ## Step 1: Disable customer account API Comment out customer account GraphQL configuration #### File: [.graphqlrc.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/.graphqlrc.ts) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/.graphqlrc.ts.dbb3fe.patch)) ```diff @@ -17,10 +17,11 @@ export default { ], }, - customer: { - schema: getSchema('customer-account'), - documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'], - }, + // Customer account API - commented out for Express recipe + // customer: { + // schema: getSchema('customer-account'), + // documents: ['./app/graphql/customer-account/*.{ts,tsx,js,jsx}'], + // }, // Add your own GraphQL projects here for CMS, Shopify Admin API, etc. }, ``` *** ## Step 2: Update README for Express deployment Update README with Express-specific setup and deployment instructions #### File: [README.md](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/README.md) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/README.md.db10ed.patch)) ## File ````diff @@ -1,45 +1,89 @@ -# Hydrogen template: Skeleton +# Hydrogen Express Skeleton -Hydrogen is Shopify’s stack for headless commerce. Hydrogen is designed to dovetail with [Remix](https://remix.run/), Shopify’s full stack web framework. This template contains a **minimal setup** of components, queries and tooling to get started with Hydrogen. +This is a Hydrogen skeleton template configured to run with NodeJS [Express](https://expressjs.com/) instead of Shopify Oxygen. + +Hydrogen is Shopify's stack for headless commerce, designed to dovetail with [React Router](https://reactrouter.com/), the full stack web framework. This template contains a **minimal Express setup** with basic components and routes to get started with Hydrogen on Node.js. [Check out Hydrogen docs](https://shopify.dev/custom-storefronts/hydrogen) -[Get familiar with Remix](https://remix.run/docs/en/v1) +[Get familiar with React Router](https://reactrouter.com/en/main) ## What's included -- Remix +- React Router 7 - Hydrogen -- Oxygen +- Express server - Vite -- Shopify CLI +- TypeScript - ESLint -- Prettier -- GraphQL generator -- TypeScript and JavaScript flavors - Minimal setup of components and routes +## Important Notes + +This Express setup differs from the standard Hydrogen template: + +1. **Cache Implementation**: Uses an in-memory cache. In production, you should implement redis, memcached, or another cache that implements the [Cache interface](https://developer.mozilla.org/en-US/docs/Web/API/Cache). +2. **Storefront Redirect**: Does not utilize [`storefrontRedirect`](https://shopify.dev/docs/api/hydrogen/utilities/storefrontredirect) functionality. +3. **Minimal Routes**: Only includes index and product routes. Add more routes as needed. + ## Getting started **Requirements:** -- Node.js version 18.0.0 or higher +- Node.js version 18.0.0 or higher (but less than 22.0.0) + +### Environment Setup + +Create a `.env` file with your Shopify store credentials: + +```env +PUBLIC_STOREFRONT_API_TOKEN="your-token" +PUBLIC_STORE_DOMAIN="your-store.myshopify.com" +PUBLIC_STOREFRONT_ID="your-storefront-id" +SESSION_SECRET="your-session-secret-at-least-32-chars" +``` + +## Local development + +Start the Express development server: ```bash -npm create @shopify/hydrogen@latest +npm run dev ``` +This starts your app in development mode with hot module replacement. + ## Building for production ```bash npm run build ``` -## Local development +## Production deployment + +Run the app in production mode: ```bash -npm run dev +npm start ``` -## Setup for using Customer Account API (`/account` section) +### Deployment -Follow step 1 and 2 of +When deploying your Express application, ensure you deploy: + +- `build/` directory +- `server.mjs` file +- `package.json` and dependencies +- Your `.env` configuration + +The Express server runs on the port specified by the `PORT` environment variable (defaults to 3000). + +## Project Structure + +- `server.mjs` - Express server configuration +- `scripts/dev.mjs` - Development server orchestration +- `app/` - React Router application code + - `entry.client.tsx` - Client-side entry point + - `entry.server.tsx` - Server-side rendering entry point + - `root.tsx` - Root layout component + - `routes/` - Application routes +- `build/` - Production build output (generated) \ No newline at end of file ```` *** ## Step 3: Add environment type definitions Add environment type definitions for Hydrogen on Express #### File: [env.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/app/env.ts) ## File ```ts // This file extends the Hydrogen types for this project // The types are automatically available via @shopify/hydrogen/react-router-types // Extend the session data for your app declare module 'react-router' { interface SessionData { customerAccessToken?: string; cartId?: string; } } // Extend the environment variables for your app declare global { interface Env { // Your custom environment variables SOME_API_KEY?: string; } } // Add additional context properties if needed declare global { interface HydrogenAdditionalContext { // Add any custom context properties your app needs // For example: // cms?: CMSClient; } } // Required to make this file a module and enable the augmentation export {}; ``` *** ## Step 4: Set up client-side hydration Update client entry to use React Router hydration without Oxygen-specific code #### File: [entry.client.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/entry.client.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/entry.client.tsx.0b6923.patch)) ```diff @@ -1,21 +1,13 @@ import {HydratedRouter} from 'react-router/dom'; import {startTransition, StrictMode} from 'react'; import {hydrateRoot} from 'react-dom/client'; -import {NonceProvider} from '@shopify/hydrogen'; if (!window.location.origin.includes('webcache.googleusercontent.com')) { startTransition(() => { - // Extract nonce from existing script tags - const existingNonce = document - .querySelector('script[nonce]') - ?.nonce; - hydrateRoot( document, - - - + , ); }); ``` *** ## Step 5: Add the Express template favicon Add Express template favicon #### File: [favicon.svg](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/public/favicon.svg) ## File ```svg ``` *** ## Step 6: Configure server-side rendering Replace Oxygen server rendering with Express-compatible Node.js SSR using PassThrough streams #### File: [entry.server.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/entry.server.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/entry.server.tsx.b35f11.patch)) ## File ```diff @@ -1,53 +1,77 @@ +import {PassThrough} from 'node:stream'; +import type {EntryContext} from 'react-router'; +import {createReadableStreamFromReadable} from '@react-router/node'; import {ServerRouter} from 'react-router'; import {isbot} from 'isbot'; -import {renderToReadableStream} from 'react-dom/server'; +import type {RenderToPipeableStreamOptions} from 'react-dom/server'; +import {renderToPipeableStream} from 'react-dom/server'; import { createContentSecurityPolicy, type HydrogenRouterContextProvider, } from '@shopify/hydrogen'; -import type {EntryContext} from 'react-router'; -export default async function handleRequest( +const ABORT_DELAY = 5_000; + +export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, reactRouterContext: EntryContext, context: HydrogenRouterContextProvider, ) { - const {nonce, header, NonceProvider} = createContentSecurityPolicy({ - shop: { - checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN, - storeDomain: context.env.PUBLIC_STORE_DOMAIN, - }, - }); - - const body = await renderToReadableStream( - - - , - { - nonce, - signal: request.signal, - onError(error) { - console.error(error); - responseStatusCode = 500; + return new Promise((resolve, reject) => { + const {nonce, header, NonceProvider} = createContentSecurityPolicy({ + shop: { + checkoutDomain: context.env.PUBLIC_CHECKOUT_DOMAIN, + storeDomain: context.env.PUBLIC_STORE_DOMAIN, }, - }, - ); + }); - if (isbot(request.headers.get('user-agent'))) { - await body.allReady; - } + let shellRendered = false; + const userAgent = request.headers.get('user-agent'); - responseHeaders.set('Content-Type', 'text/html'); - responseHeaders.set('Content-Security-Policy', header); + const readyOption: keyof RenderToPipeableStreamOptions = + userAgent && isbot(userAgent) ? 'onAllReady' : 'onShellReady'; - return new Response(body, { - headers: responseHeaders, - status: responseStatusCode, + const {pipe, abort} = renderToPipeableStream( + + + , + { + nonce, + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + responseHeaders.set('Content-Security-Policy', header); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + if (shellRendered) { + console.error(error); + } + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); }); -} +} \ No newline at end of file ``` *** ## Step 7: Set up the development server Add development server orchestration script for Vite and nodemon #### File: [dev.mjs](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/scripts/dev.mjs) ## File ```mjs #!/usr/bin/env node import {spawn} from 'child_process'; import {watch} from 'fs'; import {join, dirname} from 'path'; import {fileURLToPath} from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const rootDir = join(__dirname, '..'); // Start the Express server const server = spawn('npm', ['run', 'dev:server'], { stdio: 'inherit', shell: true, cwd: rootDir, }); // Run initial type generation with --watch flag to avoid WebSocket conflicts console.log('🔄 Generating React Router types...'); const initialTypegen = spawn('npx', ['react-router', 'typegen'], { stdio: ['inherit', 'inherit', 'pipe'], // Pipe stderr to suppress WebSocket warnings shell: true, cwd: rootDir, }); // Filter out WebSocket errors from stderr initialTypegen.stderr?.on('data', (data) => { const message = data.toString(); if (!message.includes('WebSocket server error')) { process.stderr.write(data); } }); initialTypegen.on('close', () => { console.log('✅ Initial types generated'); // Show dev server URL const port = process.env.PORT || 3000; console.log('\n🚀 Express server ready!\n'); console.log(` ➜ Local: http://localhost:${port}`); console.log(` ➜ Network: use --host to expose\n`); }); // Watch for route changes const routesDir = join(rootDir, 'app', 'routes'); const routesFile = join(rootDir, 'app', 'routes.ts'); console.log('👀 Watching for route changes...'); let typegenTimeout; const runTypegen = () => { clearTimeout(typegenTimeout); typegenTimeout = setTimeout(() => { console.log('🔄 Route change detected, regenerating types...'); const typegen = spawn('npx', ['react-router', 'typegen'], { stdio: ['inherit', 'inherit', 'pipe'], shell: true, cwd: rootDir, }); // Filter out WebSocket errors typegen.stderr?.on('data', (data) => { const message = data.toString(); if (!message.includes('WebSocket server error')) { process.stderr.write(data); } }); typegen.on('close', (code) => { if (code === 0) { console.log('✅ Types regenerated'); } else { console.error('❌ Type generation failed'); } }); }, 500); // Debounce for 500ms }; // Watch routes directory watch(routesDir, {recursive: true}, (eventType, filename) => { if (filename && (filename.endsWith('.tsx') || filename.endsWith('.ts'))) { runTypegen(); } }); // Watch routes.ts file watch(routesFile, () => { runTypegen(); }); // Handle cleanup process.on('SIGINT', () => { console.log('\n🛑 Shutting down...'); server.kill(); process.exit(0); }); process.on('SIGTERM', () => { server.kill(); process.exit(0); }); ``` *** ## Step 8: Simplify the root layout Simplify root layout for Express template by removing complex components #### File: [root.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/root.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/root.tsx.5e9998.patch)) ## File ```diff @@ -11,46 +11,20 @@ import { useRouteLoaderData, } from 'react-router'; import type {Route} from './+types/root'; -import favicon from '~/assets/favicon.svg'; -import {FOOTER_QUERY, HEADER_QUERY} from '~/lib/fragments'; -import resetStyles from '~/styles/reset.css?url'; -import appStyles from '~/styles/app.css?url'; -import {PageLayout} from './components/PageLayout'; +import styles from './styles/app.css?url'; export type RootLoader = typeof loader; -/** - * This is important to avoid re-fetching root queries on sub-navigations - */ export const shouldRevalidate: ShouldRevalidateFunction = ({ formMethod, currentUrl, nextUrl, }) => { - // revalidate when a mutation is performed e.g add to cart, login... if (formMethod && formMethod !== 'GET') return true; - - // revalidate when manually revalidating via useRevalidator if (currentUrl.toString() === nextUrl.toString()) return true; - - // Defaulting to no revalidation for root loader data to improve performance. - // When using this feature, you risk your UI getting out of sync with your server. - // Use with caution. If you are uncomfortable with this optimization, update the - // line below to `return defaultShouldRevalidate` instead. - // For more details see: https://remix.run/docs/en/main/route/should-revalidate return false; }; -/** ``` *** ## Step 9: Create the Express server Add Express server with Hydrogen context, session management, and SSR support #### File: [server.mjs](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/ingredients/templates/skeleton/server.mjs) ## File ```mjs import {createRequestHandler} from '@react-router/express'; import {createCookieSessionStorage} from 'react-router'; import compression from 'compression'; import express from 'express'; import morgan from 'morgan'; import {createHydrogenContext, InMemoryCache} from '@shopify/hydrogen'; // Don't capture process.env too early - it needs to be accessed after dotenv loads const getEnv = () => process.env; let vite; if (process.env.NODE_ENV !== 'production') { const {createServer} = await import('vite'); vite = await createServer({ server: { middlewareMode: true, }, configFile: 'vite.config.ts', }); } const app = express(); app.use(compression()); // http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header app.disable('x-powered-by'); // handle asset requests if (vite) { app.use(vite.middlewares); } else { // add morgan here for production only // dev uses morgan plugin, otherwise it spams the console with HMR requests app.use(morgan('tiny')); app.use( ``` *** ## Step 10: Configure routes for Express Update routes configuration to work with Hydrogen on Express #### File: [routes.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes.ts) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/routes.ts.87938f.patch)) ```diff @@ -1,9 +1,8 @@ import {flatRoutes} from '@react-router/fs-routes'; import {type RouteConfig} from '@react-router/dev/routes'; -import {hydrogenRoutes} from '@shopify/hydrogen'; -export default hydrogenRoutes([ - ...(await flatRoutes()), - // Manual route definitions can be added to this array, in addition to or instead of using the `flatRoutes` file-based routing convention. - // See https://reactrouter.com/api/framework-conventions/routes.ts#routests -]) satisfies RouteConfig; +export default (async () => { + const {hydrogenRoutes} = await import('@shopify/hydrogen'); + const routes = await flatRoutes(); + return hydrogenRoutes([...routes]); +})() satisfies Promise; ``` *** ## Step 11: Create a basic homepage Simplify homepage route to basic Express example content #### File: [\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes/_index.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/_index.tsx.243e26.patch)) ## File ```diff @@ -1,171 +1,28 @@ -import { - Await, - useLoaderData, - Link, -} from 'react-router'; -import type {Route} from './+types/_index'; -import {Suspense} from 'react'; -import {Image} from '@shopify/hydrogen'; -import type { - FeaturedCollectionFragment, - RecommendedProductsQuery, -} from 'storefrontapi.generated'; -import {ProductItem} from '~/components/ProductItem'; +import {useRouteError, isRouteErrorResponse, Link} from 'react-router'; -export const meta: Route.MetaFunction = () => { - return [{title: 'Hydrogen | Home'}]; -}; - -export async function loader(args: Route.LoaderArgs) { - // Start fetching non-critical data without blocking time to first byte - const deferredData = loadDeferredData(args); - - // Await the critical data required to render initial state of the page - const criticalData = await loadCriticalData(args); - - return {...deferredData, ...criticalData}; -} - -/** - * Load data necessary for rendering content above the fold. This is the critical data - * needed to render the page. If it's unavailable, the whole page should 400 or 500 error. - */ -async function loadCriticalData({context}: Route.LoaderArgs) { - const [{collections}] = await Promise.all([ ``` *** ## Step 12: Add a minimal product page Simplify product route to minimal implementation without cart functionality #### File: [products.$handle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/routes/products.$handle.tsx) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/products.$handle.tsx.6f2e82.patch)) ## File ```diff @@ -1,50 +1,7 @@ -import { - redirect, - useLoaderData, -} from 'react-router'; +import {useLoaderData} from 'react-router'; import type {Route} from './+types/products.$handle'; -import { - getSelectedProductOptions, - Analytics, - useOptimisticVariant, - getProductOptions, - getAdjacentAndFirstAvailableVariants, - useSelectedOptionInUrlParam, -} from '@shopify/hydrogen'; -import {ProductPrice} from '~/components/ProductPrice'; -import {ProductImage} from '~/components/ProductImage'; -import {ProductForm} from '~/components/ProductForm'; -import {redirectIfHandleIsLocalized} from '~/lib/redirect'; -export const meta: Route.MetaFunction = ({data}) => { - return [ - {title: `Hydrogen | ${data?.product.title ?? ''}`}, - { - rel: 'canonical', - href: `/products/${data?.product.handle}`, - }, - ]; -}; - -export async function loader(args: Route.LoaderArgs) { - // Start fetching non-critical data without blocking time to first byte - const deferredData = loadDeferredData(args); - - // Await the critical data required to render initial state of the page - const criticalData = await loadCriticalData(args); ``` *** ## Step 13: Add basic styles Replace skeleton styles with minimal Express template styling #### File: [app.css](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/app/styles/app.css) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/app.css.881a99.patch)) ## File ```diff @@ -1,574 +1,44 @@ -:root { - --aside-width: 400px; - --cart-aside-summary-height-with-discount: 300px; - --cart-aside-summary-height: 250px; - --grid-item-width: 355px; - --header-height: 64px; - --color-dark: #000; - --color-light: #fff; -} - -img { - border-radius: 4px; -} - -/* -* -------------------------------------------------- -* Non anchor links -* -------------------------------------------------- -*/ -.link:hover { - text-decoration: underline; - cursor: pointer; -} - -/* -* -------------------------------------------------- -* components/Aside -* -------------------------------------------------- -*/ -@media (max-width: 45em) { - html:has(.overlay.expanded) { - overflow: hidden; - } -} - ``` *** ## Step 14: Update ESLint configuration Simplify ESLint configuration for Express template #### File: [eslint.config.js](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/eslint.config.js) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/eslint.config.js.e5f62b.patch)) ## File ```diff @@ -1,246 +1,2 @@ -import {fixupConfigRules, fixupPluginRules} from '@eslint/compat'; -import eslintComments from 'eslint-plugin-eslint-comments'; -import react from 'eslint-plugin-react'; -import reactHooks from 'eslint-plugin-react-hooks'; -import jsxA11Y from 'eslint-plugin-jsx-a11y'; -import globals from 'globals'; -import typescriptEslint from '@typescript-eslint/eslint-plugin'; -import _import from 'eslint-plugin-import'; -import tsParser from '@typescript-eslint/parser'; -import jest from 'eslint-plugin-jest'; -import path from 'node:path'; -import {fileURLToPath} from 'node:url'; -import js from '@eslint/js'; -import {FlatCompat} from '@eslint/eslintrc'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, -}); - -export default [ - { - ignores: [ - '**/node_modules/', - '**/build/', - '**/dist/', - '**/*.graphql.d.ts', - '**/*.graphql.ts', - '**/*.generated.d.ts', - '**/.react-router/', - '**/packages/hydrogen/dist/', - ], ``` *** ## Step 15: Install Express dependencies Update dependencies and scripts for Express server deployment (add express, nodemon, compression, remove Oxygen packages) #### File: [package.json](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/package.json) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/package.json.f30b0a.patch)) ## File ```diff @@ -5,58 +5,51 @@ "version": "2025.7.0", "type": "module", "scripts": { - "build": "shopify hydrogen build --codegen", - "dev": "shopify hydrogen dev --codegen", - "preview": "shopify hydrogen preview --build", - "lint": "eslint --no-error-on-unmatched-pattern .", + "build": "react-router typegen && react-router build", + "dev": "node ./scripts/dev.mjs", + "dev:server": "cross-env NODE_ENV=development nodemon --require dotenv/config ./server.mjs --watch ./server.mjs", + "start": "cross-env NODE_ENV=production node ./server.mjs", "typecheck": "react-router typegen && tsc --noEmit", "codegen": "shopify hydrogen codegen && react-router typegen" }, "prettier": "@shopify/prettier-config", "dependencies": { + "@react-router/express": "7.9.2", + "@react-router/node": "7.9.2", + "@remix-run/eslint-config": "^2.16.1", "@shopify/hydrogen": "2025.7.0", + "compression": "^1.7.4", + "cross-env": "^7.0.3", + "express": "^4.19.2", "graphql": "^16.10.0", "graphql-tag": "^2.12.6", "isbot": "^5.1.22", + "morgan": "^1.10.0", "react": "18.3.1", "react-dom": "18.3.1", "react-router": "7.9.2", "react-router-dom": "7.9.2" }, "devDependencies": { - "@eslint/compat": "^1.2.5", - "@eslint/eslintrc": "^3.2.0", "@eslint/js": "^9.18.0", "@graphql-codegen/cli": "5.0.2", "@react-router/dev": "7.9.2", "@react-router/fs-routes": "7.9.2", "@shopify/cli": "3.85.4", "@shopify/hydrogen-codegen": "^0.3.3", - "@shopify/mini-oxygen": "^4.0.0", - "@shopify/oxygen-workers-types": "^4.1.6", - "@shopify/prettier-config": "^1.1.2", - "@total-typescript/ts-reset": "^0.6.1", - "@types/eslint": "^9.6.1", + "@types/compression": "^1.7.2", + "@types/express": "^4.17.17", + "@types/morgan": "^1.9.4", "@types/react": "^18.2.22", "@types/react-dom": "^18.2.7", - "@typescript-eslint/eslint-plugin": "^8.21.0", - "@typescript-eslint/parser": "^8.21.0", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-import-resolver-typescript": "^3.7.0", - "eslint-plugin-eslint-comments": "^3.2.0", - "eslint-plugin-import": "^2.31.0", - "eslint-plugin-jest": "^28.11.0", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "globals": "^15.14.0", - "prettier": "^3.4.2", + "dotenv": "^16.0.3", + "nodemon": "^2.0.22", + "npm-run-all": "^4.1.5", "typescript": "^5.9.2", "vite": "^6.2.4", "vite-tsconfig-paths": "^4.3.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0 <22.0.0" } } ``` *** ## Step 16: Configure Vite for Node.​js Configure Vite for Express deployment with Node.js module externalization #### File: [vite.config.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/vite.config.ts) ([patch](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/cookbook/recipes/express/patches/vite.config.ts.475b4c.patch)) ```diff @@ -5,13 +5,15 @@ import {reactRouter} from '@react-router/dev/vite'; import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ - plugins: [hydrogen(), oxygen(), reactRouter(), tsconfigPaths()], + plugins: [hydrogen(), reactRouter(), tsconfigPaths()], build: { // Allow a strict Content-Security-Policy // withtout inlining assets as base64: assetsInlineLimit: 0, + target: 'esnext', }, ssr: { + external: ['fs', 'path', 'stream', 'crypto', 'util'], optimizeDeps: { /** * Include dependencies here if they throw CJS<>ESM errors. @@ -23,10 +25,7 @@ export default defineConfig({ * Include 'example-dep' in the array below. * @see https://vitejs.dev/config/dep-optimization-options */ - include: ['set-cookie-parser', 'cookie', 'react-router'], + include: ['@react-router/node', '@react-router/express'], }, }, - server: { - allowedHosts: ['.tryhydrogen.dev'], - }, }); ``` *** ## Deleted Files * [templates/skeleton/app/components/AddToCartButton.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/AddToCartButton.tsx) * [templates/skeleton/app/components/Aside.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/Aside.tsx) * [templates/skeleton/app/components/CartLineItem.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/CartLineItem.tsx) * [templates/skeleton/app/components/CartMain.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/CartMain.tsx) * [templates/skeleton/app/components/CartSummary.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/CartSummary.tsx) * [templates/skeleton/app/components/Footer.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/Footer.tsx) * [templates/skeleton/app/components/Header.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/Header.tsx) * [templates/skeleton/app/components/PageLayout.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/PageLayout.tsx) * [templates/skeleton/app/components/PaginatedResourceSection.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/PaginatedResourceSection.tsx) * [templates/skeleton/app/components/ProductForm.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/ProductForm.tsx) * [templates/skeleton/app/components/ProductImage.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/ProductImage.tsx) * [templates/skeleton/app/components/ProductItem.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/ProductItem.tsx) * [templates/skeleton/app/components/ProductPrice.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/ProductPrice.tsx) * [templates/skeleton/app/components/SearchForm.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/SearchForm.tsx) * [templates/skeleton/app/components/SearchFormPredictive.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/SearchFormPredictive.tsx) * [templates/skeleton/app/components/SearchResults.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/SearchResults.tsx) * [templates/skeleton/app/components/SearchResultsPredictive.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/components/SearchResultsPredictive.tsx) * [templates/skeleton/app/graphql/customer-account/CustomerAddressMutations.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/graphql/customer-account/CustomerAddressMutations.ts) * [templates/skeleton/app/graphql/customer-account/CustomerDetailsQuery.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/graphql/customer-account/CustomerDetailsQuery.ts) * [templates/skeleton/app/graphql/customer-account/CustomerOrderQuery.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/graphql/customer-account/CustomerOrderQuery.ts) * [templates/skeleton/app/graphql/customer-account/CustomerOrdersQuery.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/graphql/customer-account/CustomerOrdersQuery.ts) * [templates/skeleton/app/graphql/customer-account/CustomerUpdateMutation.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/graphql/customer-account/CustomerUpdateMutation.ts) * [templates/skeleton/app/lib/context.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/lib/context.ts) * [templates/skeleton/app/lib/fragments.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/lib/fragments.ts) * [templates/skeleton/app/lib/redirect.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/lib/redirect.ts) * [templates/skeleton/app/lib/search.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/lib/search.ts) * [templates/skeleton/app/lib/session.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/lib/session.ts) * [templates/skeleton/app/lib/variants.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/lib/variants.ts) * [templates/skeleton/app/routes/$.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/$.tsx) * [templates/skeleton/app/routes/\[robots.txt\].tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/\[robots.txt].tsx) * [templates/skeleton/app/routes/\[sitemap.xml\].tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/\[sitemap.xml].tsx) * [templates/skeleton/app/routes/account.$.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account.$.tsx) * [templates/skeleton/app/routes/account.\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account._index.tsx) * [templates/skeleton/app/routes/account.addresses.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account.addresses.tsx) * [templates/skeleton/app/routes/account.orders.$id.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account.orders.$id.tsx) * [templates/skeleton/app/routes/account.orders.\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account.orders._index.tsx) * [templates/skeleton/app/routes/account.profile.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account.profile.tsx) * [templates/skeleton/app/routes/account.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account.tsx) * [templates/skeleton/app/routes/account\_.authorize.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account_.authorize.tsx) * [templates/skeleton/app/routes/account\_.login.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account_.login.tsx) * [templates/skeleton/app/routes/account\_.logout.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/account_.logout.tsx) * [templates/skeleton/app/routes/api.$version.\[graphql.json\].tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/api.$version.\[graphql.json].tsx) * [templates/skeleton/app/routes/blogs.$blogHandle.$articleHandle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/blogs.$blogHandle.$articleHandle.tsx) * [templates/skeleton/app/routes/blogs.$blogHandle.\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/blogs.$blogHandle._index.tsx) * [templates/skeleton/app/routes/blogs.\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/blogs._index.tsx) * [templates/skeleton/app/routes/cart.$lines.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/cart.$lines.tsx) * [templates/skeleton/app/routes/cart.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/cart.tsx) * [templates/skeleton/app/routes/collections.$handle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/collections.$handle.tsx) * [templates/skeleton/app/routes/collections.\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/collections._index.tsx) * [templates/skeleton/app/routes/collections.all.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/collections.all.tsx) * [templates/skeleton/app/routes/discount.$code.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/discount.$code.tsx) * [templates/skeleton/app/routes/pages.$handle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/pages.$handle.tsx) * [templates/skeleton/app/routes/policies.$handle.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/policies.$handle.tsx) * [templates/skeleton/app/routes/policies.\_index.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/policies._index.tsx) * [templates/skeleton/app/routes/search.tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/search.tsx) * [templates/skeleton/app/routes/sitemap.$type.$page\[.xml\].tsx](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/routes/sitemap.$type.$page\[.xml].tsx) * [templates/skeleton/app/styles/reset.css](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/app/styles/reset.css) * [templates/skeleton/customer-accountapi.generated.d.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/customer-accountapi.generated.d.ts) * [templates/skeleton/env.d.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/env.d.ts) * [templates/skeleton/server.ts](https://github.com/Shopify/hydrogen/blob/12374c8f03f82c6800000cf08e327c4db4c287bb/templates/skeleton/templates/skeleton/server.ts) *** ## Next steps 1. Create a `.env` file with your Shopify Storefront API credentials: PUBLIC\_STOREFRONT\_API\_TOKEN="your-token" PUBLIC\_STORE\_DOMAIN="your-store.myshopify.com" PUBLIC\_STOREFRONT\_ID="your-storefront-id" SESSION\_SECRET="your-session-secret-at-least-32-chars" 1. Run `npm install` to install Express and other Node.js dependencies 2. Run `npm run dev` to start the development server with hot reload 3. For production, run `npm run build` followed by `npm start` 4. Deploy to your preferred Node.js hosting platform (Heroku, AWS, Vercel, Railway, etc.) *** * [Requirements](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#requirements) * [Ingredients](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#ingredients) * [Step 1: Disable customer account API](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-1-disable-customer-account-api) * [Step 2: Update README for Express deployment](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-2-update-readme-for-express-deployment) * [Step 3: Add environment type definitions](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-3-add-environment-type-definitions) * [Step 4: Set up client-side hydration](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-4-set-up-client-side-hydration) * [Step 5: Add the Express template favicon](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-5-add-the-express-template-favicon) * [Step 6: Configure server-side rendering](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-6-configure-server-side-rendering) * [Step 7: Set up the development server](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-7-set-up-the-development-server) * [Step 8: Simplify the root layout](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-8-simplify-the-root-layout) * [Step 9: Create the Express server](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-9-create-the-express-server) * [Step 10: Configure routes for Express](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-10-configure-routes-for-express) * [Step 11: Create a basic homepage](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-11-create-a-basic-homepage) * [Step 12: Add a minimal product page](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-12-add-a-minimal-product-page) * [Step 13: Add basic styles](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-13-add-basic-styles) * [Step 14: Update ESLint configuration](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-14-update-eslint-configuration) * [Step 15: Install Express dependencies](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-15-install-express-dependencies) * [Step 16: Configure Vite for Node.​js](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#step-16-configure-vite-for-nodejs) * [Deleted Files](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#deleted-files) * [Next steps](https://shopify.dev/docs/storefronts/headless/hydrogen/cookbook/express.md#next-steps)