Skip to main content

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

  • Node.js 20 or higher (less than 22.0.0) for production deployment
  • npm or yarn package manager
  • Shopify Storefront API credentials

New files added to the template by this recipe.

FileDescription
app/env.tsEnvironment type definitions for Express server
public/favicon.svgFavicon for Express template
scripts/dev.mjsDevelopment orchestration script
server.mjsExpress server with Hydrogen context and SSR

Anchor to Step 1: Disable customer account APIStep 1: Disable customer account API

Comment out customer account GraphQL configuration

@@ -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.
},

Anchor to Step 2: Update README for Express deploymentStep 2: Update README for Express deployment

Update README with Express-specific setup and deployment instructions

File

@@ -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 <https://shopify.dev/docs/custom-storefronts/building-with-the-customer-account-api/hydrogen#step-1-set-up-a-public-domain-for-local-development>
+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

Anchor to Step 3: Add environment type definitionsStep 3: Add environment type definitions

Add environment type definitions for Hydrogen on Express

File

// 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 {};

Anchor to Step 4: Set up client-side hydrationStep 4: Set up client-side hydration

Update client entry to use React Router hydration without Oxygen-specific code

@@ -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<HTMLScriptElement>('script[nonce]')
- ?.nonce;
-
hydrateRoot(
document,
<StrictMode>
- <NonceProvider value={existingNonce}>
- <HydratedRouter />
- </NonceProvider>
+ <HydratedRouter />
</StrictMode>,
);
});

Anchor to Step 5: Add the Express template faviconStep 5: Add the Express template favicon

Add Express template favicon

File

<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none">
<style>
.stroke {
stroke: #000;
}
.fill {
fill: #000;
}
@media (prefers-color-scheme: dark) {
.stroke {
stroke: #fff;
}
.fill {
fill: #fff;
}
}
</style>
<path
class="stroke"
fill-rule="evenodd"
d="M16.1 16.04 1 8.02 6.16 5.3l5.82 3.09 4.88-2.57-5.82-3.1L16.21 0l15.1 8.02-5.17 2.72-5.5-2.91-4.88 2.57 5.5 2.92-5.16 2.72Z"
/>
<path
class="fill"
fill-rule="evenodd"
d="M16.1 32 1 23.98l5.16-2.72 5.82 3.08 4.88-2.57-5.82-3.08 5.17-2.73 15.1 8.02-5.17 2.72-5.5-2.92-4.88 2.58 5.5 2.92L16.1 32Z"
/>
</svg>

Anchor to Step 6: Configure server-side renderingStep 6: Configure server-side rendering

Replace Oxygen server rendering with Express-compatible Node.js SSR using PassThrough streams

File

@@ -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(
- <NonceProvider>
- <ServerRouter
- context={reactRouterContext}
- url={request.url}
- nonce={nonce}
- />
- </NonceProvider>,
- {
- 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(
+ <NonceProvider>
+ <ServerRouter
+ context={reactRouterContext}
+ url={request.url}
+ nonce={nonce}
+ />
+ </NonceProvider>,
+ {
+ 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

Anchor to Step 7: Set up the development serverStep 7: Set up the development server

Add development server orchestration script for Vite and nodemon

File

#!/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);
});

Anchor to Step 8: Simplify the root layoutStep 8: Simplify the root layout

Simplify root layout for Express template by removing complex components

File

@@ -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;
};
-/**

Anchor to Step 9: Create the Express serverStep 9: Create the Express server

Add Express server with Hydrogen context, session management, and SSR support

File

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(

Anchor to Step 10: Configure routes for ExpressStep 10: Configure routes for Express

Update routes configuration to work with Hydrogen on Express

@@ -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<RouteConfig>;

Anchor to Step 11: Create a basic homepageStep 11: Create a basic homepage

Simplify homepage route to basic Express example content

File

@@ -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([

Anchor to Step 12: Add a minimal product pageStep 12: Add a minimal product page

Simplify product route to minimal implementation without cart functionality

File

@@ -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);

Anchor to Step 13: Add basic stylesStep 13: Add basic styles

Replace skeleton styles with minimal Express template styling

File

@@ -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;
- }
-}
-

Anchor to Step 14: Update ESLint configurationStep 14: Update ESLint configuration

Simplify ESLint configuration for Express template

File

@@ -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/',
- ],

Anchor to Step 15: Install Express dependenciesStep 15: Install Express dependencies

Update dependencies and scripts for Express server deployment (add express, nodemon, compression, remove Oxygen packages)

File

@@ -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"
}
}

Anchor to Step 16: Configure Vite for Node.jsStep 16: Configure Vite for Node.js

Configure Vite for Express deployment with Node.js module externalization

@@ -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'],
- },
});


  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.)

Was this page helpful?