Overview
While React Router provides adapters for common platforms, you can create custom server integrations for any environment that supports the Web Fetch API.
Core Concepts
All React Router server adapters use the same underlying pattern:
- Import your server build
- Create a request handler with
createRequestHandler
- Convert platform requests to Web API
Request objects
- Convert Web API
Response objects back to platform responses
Basic Pattern
Here’s the general pattern for any server adapter:
import { createRequestHandler } from "react-router";
import type { ServerBuild } from "react-router";
// Import your server build
const build = await import("./build/server/index.js");
// Create the request handler
const handleRequest = createRequestHandler(build);
// Convert platform request to Web API Request
function toPlatformRequest(platformReq): Request {
const url = new URL(platformReq.url, `http://${platformReq.headers.host}`);
return new Request(url.href, {
method: platformReq.method,
headers: platformReq.headers,
body: platformReq.body,
});
}
// Convert Web API Response to platform response
async function fromPlatformResponse(response: Response, platformRes) {
platformRes.statusCode = response.status;
platformRes.statusMessage = response.statusText;
for (const [key, value] of response.headers.entries()) {
platformRes.setHeader(key, value);
}
if (response.body) {
// Stream response body
} else {
platformRes.end();
}
}
Node.js HTTP Server
Create a custom Node.js server without using the @react-router/node package:
import { createServer } from "node:http";
import { createRequestHandler } from "react-router";
const build = await import("./build/server/index.js");
const handleRequest = createRequestHandler(build);
const server = createServer(async (req, res) => {
try {
// Build Web API Request
const request = new Request(
new URL(req.url!, `http://${req.headers.host}`).href,
{
method: req.method,
headers: new Headers(req.headers as Record<string, string>),
body:
req.method !== "GET" && req.method !== "HEAD"
? req
: undefined,
}
);
// Get Web API Response
const response = await handleRequest(request);
// Send response
res.statusCode = response.status;
res.statusMessage = response.statusText;
for (const [key, value] of response.headers.entries()) {
res.setHeader(key, value);
}
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
res.write(value);
}
}
res.end();
} catch (error) {
console.error(error);
res.statusCode = 500;
res.end("Internal Server Error");
}
});
server.listen(3000);
Deno Server
Create a server for Deno:
import { createRequestHandler } from "react-router";
import * as build from "./build/server/index.js";
const handleRequest = createRequestHandler(build);
Deno.serve({ port: 3000 }, async (request) => {
try {
return await handleRequest(request);
} catch (error) {
console.error(error);
return new Response("Internal Server Error", { status: 500 });
}
});
Bun Server
Create a server for Bun:
import { createRequestHandler } from "react-router";
import * as build from "./build/server/index.js";
const handleRequest = createRequestHandler(build);
Bun.serve({
port: 3000,
async fetch(request) {
try {
return await handleRequest(request);
} catch (error) {
console.error(error);
return new Response("Internal Server Error", { status: 500 });
}
},
});
Custom Load Context
Pass platform-specific values to your routes:
const handleRequest = createRequestHandler(build);
const response = await handleRequest(request, {
// Custom context available in loaders/actions
env: Deno.env.toObject(),
platform: "deno",
serverUrl: "https://example.com",
});
Access in your routes:
export async function loader({ context }: Route.LoaderArgs) {
console.log(context.platform); // "deno"
console.log(context.env.API_KEY);
return {};
}
Dynamic Build Import
For development, reload the build on each request:
const handleRequest = createRequestHandler(
// Function that returns the build
() => import("./build/server/index.js?" + Date.now())
);
For production, import once:
const build = await import("./build/server/index.js");
const handleRequest = createRequestHandler(build);
Mode Configuration
Pass the mode as the second parameter:
const handleRequest = createRequestHandler(
build,
process.env.NODE_ENV // "development" or "production"
);
This affects:
- Error stack traces (detailed in development)
- Asset URLs and caching
- Source maps
Streaming Responses
React Router uses streaming by default. Ensure your server supports streaming responses:
if (response.body) {
const reader = response.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
// Write chunk to platform response
platformRes.write(value);
}
}
platformRes.end();
Static Asset Serving
Serve your built client assets:
import { serve } from "@hono/node-server/serve-static";
// Serve static files
app.use("/assets/*", serve({ root: "./build/client" }));
// React Router handles all other routes
app.use("*", async (c) => {
const response = await handleRequest(c.req.raw);
return response;
});
Error Handling
Handle errors appropriately:
try {
const response = await handleRequest(request, context);
return response;
} catch (error) {
console.error(error);
if (process.env.NODE_ENV === "development") {
return new Response(error.stack, { status: 500 });
}
return new Response("Internal Server Error", { status: 500 });
}
Request Abortion
Support request cancellation when the client disconnects:
const controller = new AbortController();
platformReq.on("close", () => {
controller.abort();
});
const request = new Request(url, {
method: platformReq.method,
headers: platformReq.headers,
body: platformReq.body,
signal: controller.signal,
});
Example Adapters
Refer to the official adapters for implementation examples:
- Node.js:
packages/react-router-node/server.ts
- Cloudflare:
packages/react-router-cloudflare/worker.ts
- Express:
packages/react-router-express/server.ts
Testing Your Adapter
-
Build your application:
-
Run your custom server:
-
Test server-side rendering:
curl http://localhost:3000
-
Verify static assets load correctly
-
Test dynamic routes and data loading
Custom servers must support the Web Fetch API (Request/Response). Modern runtimes like Node.js 18+, Deno, and Bun have built-in support.