react-router-hono-server v2.11.0
React Router v7 Hono Server
Psst!
!IMPORTANT This package is only compatible with React Router v7
You can still use the v1 with @remix-run. Previous docs
Migration guide from v1 here
TLDR
This package contains a helper function createHonoServer
that enables you to create a Hono
server bound to your React Router v7 app.
Since the Hono server is built along with the rest of your app, you may import app modules as needed.
It also supports Vite HMR via the react-router-hono-server/dev
plugin (which is required
for this to function).
It presets a default Hono server config that you can customize
!IMPORTANT Only works with React Router v7 in ESM mode
Only works with Vite
Only Node, Bun, Cloudflare Workers and AWS Lambda (experimental) are supported
!TIP šØāš« There is some examples in the examples folder. I hope they will help you.
You can use remix-hono to add cool middleware like
session
Installation
Install the following npm package.
!NOTE This is not a dev dependency, as it creates the Hono server used in production.
npm install react-router-hono-server
# For Cloudflare Workers, add the following
npm install -D miniflare wrangler
!TIP You don't need to install
hono
as it is included in this package.If you use
pnpm
, and want to use some imports fromhono
, you may need to installhono
manually or create a.npmrc
file in your project with the following content:public-hoist-pattern[]=hono
Easy mode
In your vite.config.ts
, add the reactRouterHonoServer
plugin.
import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev"; // add this
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
reactRouterHonoServer(), // add this
reactRouter(),
tsconfigPaths()
],
});
That's all!
Wait, what?
For really simple apps, that's all you need to do. Behind the hood, it will create a virtual module with a default Hono server.
When building for production, it will create the server file at build/server/index.js
and import your React Router app from virtual:react-router/server-build
module (replacing it with the real file located in build/server/assets/server-build-[hash].js
).
Configuration
Ok, by default it works, but you may want to customize the server and use some middleware.
!IMPORTANT Until you define your own
serverEntryPoint
, the file name${appDirectory}/server.ts
and the folder name${appDirectory}/server
are reserved words.
reactRouterHonoServer
plugin is looking for them to find your server file.
Add the Vite plugin
!NOTE It uses the
reactRouter
plugin to build your app and will automatically load its config.
Node
// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev"; // add this
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
reactRouterHonoServer(), // add this
reactRouter(),
tsconfigPaths()
],
});
Bun
!TIP Check this example to see how to use it.
!IMPORTANT React 19 and
bun --bun
flag require a specialentry.server.tsx
file. Check Usingbun --bun
flag? Check this example to see how to use it.
// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev"; // add this
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
reactRouterHonoServer({ runtime: "bun"} ), // add this
reactRouter(),
tsconfigPaths()
],
});
Using bun --bun
flag?
If you are using the bun --bun
flag, you need to have a proper entry.server.tsx
file.
When building for production, at least for React 19, it uses a special react-dom-server.bun
import with only the renderToReadableStream
function available.
import { isbot } from "isbot";
import { renderToReadableStream } from "react-dom/server.bun";
import type { AppLoadContext, EntryContext } from "react-router";
import { ServerRouter } from "react-router";
export default async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
_loadContext: AppLoadContext
) {
let shellRendered = false;
const userAgent = request.headers.get("user-agent");
const body = await renderToReadableStream(<ServerRouter context={routerContext} url={request.url} />, {
onError(error: unknown) {
responseStatusCode = 500;
if (shellRendered) {
console.error(error);
}
},
});
shellRendered = true;
if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
await body.allReady;
}
responseHeaders.set("Content-Type", "text/html");
return new Response(body, {
headers: responseHeaders,
status: responseStatusCode,
});
}
Cloudflare Workers
!TIP Check this example to see how to use it.
!IMPORTANT You need to add the
cloudflareDevProxy
plugin to use the Cloudflare Workers runtime on dev.
// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare"; // add this
import { reactRouterHonoServer } from "react-router-hono-server/dev"; // add this
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
cloudflareDevProxy(),
reactRouterHonoServer({ runtime: "cloudflare"} ), // add this
reactRouter(),
tsconfigPaths()
],
});
React 19
!IMPORTANT Check this example to see how to use it.
!TIP You may need to add the
force_react_19
flag to thereactRouterHonoServer
plugin config.
// vite.config.ts
reactRouterHonoServer({ runtime: "cloudflare", flag: { force_react_19: true } }), // add this
AWS Lambda
!TIP AWS shares the same runtime as Node.
// vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev"; // add this
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
plugins: [
reactRouterHonoServer({ runtime: "aws", dev: { export: "handler" } }), // add this
reactRouter(),
tsconfigPaths()
],
});
Create the server
!TIP You can use the CLI to create the server file for you.
npx react-router-hono-server reveal file
In your app
folder, create a file named server.ts
and export as default the server created by createHonoServer
.
touch app/server.ts
// app/server.ts
import { createHonoServer } from "react-router-hono-server/node";
export default await createHonoServer({/* options */});
Alternative
You can define your server in app/server/index.ts
.
!TIP You can use the CLI to create the server file for you.
npx react-router-hono-server reveal folder
It is useful if you have many middleware and want to keep your server file clean.
// app/server/index.ts
import { createHonoServer } from "react-router-hono-server/node";
export default await createHonoServer({/* options */});
I don't like this default
No problem, you can define your files wherever you want.
Use the serverEntryPoint
option of the Vite plugin reactRouterHonoServer
to point to your server file.
Update your package.json scripts
Node
It is not an error, you can keep the React Router defaults for build
and dev
!
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "node ./build/server/index.js",
},
Bun
It is not an error, you can keep the React Router defaults for build
!
"scripts": {
"build": "react-router build",
"dev": "bunx --bun vite",
"start": "bun ./build/server/index.js",
},
Cloudflare
It is not an error, you can keep the React Router defaults for build
and dev
!
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "wrangler dev",
},
Wrangler
Add a file named wrangler.toml
at the root of your project (close to package.json
).
Adapt the main
and assets
fields based on your build output, if you changed them from the defaults.
workers_dev = true
name = "my-worker"
compatibility_date = "2024-11-18"
main = "./build/server/index.js"
assets = { directory = "./build/client/" }
Custom assets serving
You can set Cloudflare experimental_serve_directly
and delegate assets serving to Hono, like for Node and Bun.
!TIP Check https://developers.cloudflare.com/workers/static-assets/binding/#experimental_serve_directly
[assets]
directory = "./build/client/"
binding = "ASSETS"
experimental_serve_directly = false
AWS Lambda
It is not an error, you can keep the React Router defaults for build
and dev
!
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "I don't know how to start it in production mode š
",
},
How it works
This helper works differently depending on the environment.
In development, it uses @hono/vite-dev-server and loads your server and React Router app with import('virtual:react-router/server-build')
.
It can be configured in vite.config.ts
.
In production
, it will create a standard node HTTP server listening at HOST:PORT
.
You can customize the production server port using the port
option of createHonoServer
.
When building for production, the Hono server is compiled as build/server/index.js
and imports your React Router app from assets/server-build-[hash].js
.
To run the server in production, use node ./build/server/index.js
.
That's all!
Options
reactRouterHonoServer
(Vite Plugin)
type Runtime = "node" | "bun" | "cloudflare" | "aws";
type ReactRouterHonoServerPluginOptions = {
/**
* The runtime to use for the server.
*
* Defaults to `node`.
*/
runtime?: Runtime;
/**
* The path to the server file, relative to `vite.config.ts`.
*
* If it is a folder (`app/server`), it will look for an `index.ts` file.
*
* Defaults to `${appDirectory}/server[.ts | /index.ts]` if present.
*
* Fallback to a virtual module `virtual:react-router-hono-server/server`.
*/
serverEntryPoint?: string;
/**
* The paths that are not served by the dev-server.
*
* Defaults include `appDirectory` content.
*/
dev?: {
/**
* The paths that are not served by the dev-server.
*
* Defaults include `appDirectory` content.
*/
exclude?: (string | RegExp)[];
/**
* The name of the export to use for the server.
*
* Defaults to `default`.
*/
export?: string;
};
};
createHonoServer
All adapters
export type HonoServerOptions<E extends Env = BlankEnv> = {
/**
* The base Hono app to use
*
* It will be used to mount the React Router server on the `basename` path
* defined in the [React Router config](https://api.reactrouter.com/v7/types/_react_router_dev.config.Config.html)
*
* {@link Hono}
*/
app?: Hono<E>;
/**
* Enable the default logger
*
* Defaults to `true`
*/
defaultLogger?: boolean;
/**
* The port to start the server on
*
* Defaults to `process.env.PORT || 3000`
*/
port?: number;
/**
* Customize the Hono server, for example, adding middleware
*
* It is applied after the default middleware and before the React Router middleware
*/
configure?: <E extends Env = BlankEnv>(server: Hono<E>) => Promise<void> | void;
/**
* Augment the React Router AppLoadContext
*
* Don't forget to declare the AppLoadContext in your app, next to where you create the Hono server
*
* ```ts
* declare module "react-router" {
* interface AppLoadContext {
* // Add your custom context here
* whatever: string;
* }
* }
* ```
*/
getLoadContext?: (
c: Context,
options: {
build: ServerBuild;
mode: "development" | "production" | "test";
}
) => Promise<AppLoadContext> | AppLoadContext;
/**
* Hook to add middleware that runs before any built-in middleware, including assets serving.
*
* You can use it to add protection middleware, for example.
*/
beforeAll?: (app: Hono<E>) => Promise<void> | void;
};
You can add additional Hono middleware with the configure
function. If you do not provide a function, it will create a default Hono server.
The configure
function can be async. So, make sure to await createHonoServer()
.
If you want to set up the React Router AppLoadContext
, pass in a function to getLoadContext
.
Modify the AppLoadContext
interface used in your app.
Since the Hono server is compiled in the same bundle as the rest of your React Router app, you can import app modules just like you normally would.
Example
// app/server.ts
import { createHonoServer } from "react-router-hono-server/node";
/**
* Declare our loaders and actions context type
*/
declare module "react-router" {
interface AppLoadContext {
/**
* The app version from the build assets
*/
readonly appVersion: string;
}
}
export default await createHonoServer({
getLoadContext(_, { build, mode }) {
const isProductionMode = mode === "production";
return {
appVersion: isProductionMode ? build.assets.version : "dev",
};
},
});
// app/routes/test.tsx
import type { Route } from "./+types/test";
export async function loader({ context }: Route.LoaderArgs) {
// get the context provided from `getLoadContext`
return { appVersion: context.appVersion }
}
!TIP If you declare your
getLoadContext
function in a separate file, you can use the helpercreateGetLoadContext
fromreact-router-hono-server/{adapter}
import { createGetLoadContext } from "react-router-hono-server/node"; export const getLoadContext = createGetLoadContext((c, { mode, build }) => { const isProductionMode = mode === "production"; return { appVersion: isProductionMode ? build.assets.version : "dev", }; });
Node
export interface HonoServerOptions<E extends Env = BlankEnv> extends HonoServerOptionsBase<E> {
/**
* Listening listener (production mode only)
*
* It is called when the server is listening
*
* Defaults log the port
*/
listeningListener?: (info: AddressInfo) => void;
/**
* Customize the node server (ex: using http2)
*
* {@link https://hono.dev/docs/getting-started/nodejs#http2}
*/
customNodeServer?: CreateNodeServerOptions;
/**
* Callback executed just after `serve` from `@hono/node-server`
*
* **Only applied to production mode**
*
* For example, you can use this to bind `@hono/node-ws`'s `injectWebSocket`
*/
onServe?: (server: ServerType) => void;
/**
* The Node.js Adapter rewrites the global Request/Response and uses a lightweight Request/Response to improve performance.
*
* If you want this behavior, set it to `true`
*
* šØ Setting this to `true` can break `request.clone()` if you later check `instanceof Request`.
*
* {@link https://github.com/honojs/node-server?tab=readme-ov-file#overrideglobalobjects}
*
* @default false
*/
overrideGlobalObjects?: boolean;
}
Bun
export interface HonoServerOptions<E extends Env = BlankEnv> extends HonoServerOptionsBase<E> {
/**
* Customize the bun server
*
* {@link https://bun.sh/docs/api/http#start-a-server-bun-serve}
*/
customBunServer?: Serve & ServeOptions;
}
Cloudflare Workers
export interface HonoServerOptions<E extends Env = BlankEnv> extends Omit<HonoServerOptionsBase<E>, "port"> {}
AWS Lambda
type InvokeMode = "default" | "stream";
interface HonoAWSServerOptions<E extends Env = BlankEnv> extends Omit<HonoServerOptionsBase<E>, "port"> {
/**
* The invoke mode to use
*
* Defaults to `default`
*
* {@link https://hono.dev/docs/getting-started/aws-lambda#lambda-response-streaming}
*/
invokeMode: InvokeMode;
}
Middleware
šØ Redirecting from a middleware
!IMPORTANT TLDR: If you encounter a
Error: Unable to decode turbo-stream response
after a redirect from your middleware, try to useredirect
instead ofc.redirect
.
redirect
will usec.redirect
for "normal" requests and a single-fetch-like response for React Router.data
requests.If the next handler is a Hono middleware (ex: https://github.com/rphlmr/react-router-hono-server/discussions/56), you can use
c.redirect
as usual.You have to use the
redirect
helper to redirect from a middleware if the next handler that will receive this redirect is a React Router route.It returns a single-fetch-like response.
If you use
c.redirect
, it will not work as expected and you will get aUnable to decode turbo-stream response
error.import { redirect } from "react-router-hono-server/http";
I'm sorry for this inconvenience, I hope it can be "fixed" in future React Router versions.
Middleware are functions that are called before React Router calls your loader/action.
Hono is the perfect tool for this, as it supports middleware out of the box.
See the Hono docs for more information.
You can imagine many use cases for middleware, such as authentication, protecting routes, caching, logging, etc.
See how Shelf.nu uses them!
!TIP This lib exports one middleware
cache
(react-router-hono-server/middleware
) that you can use to cache your responses.
beforeAll
You can use the beforeAll
option to add middleware that runs before any built-in middleware, including assets serving.
You can use it to add protection middleware, for example.
!TIP When you check the path to protect, don't forget to use
c.req.path.includes("")
to handle.data
requests (loader
)!
import { reactRouterRedirect } from "react-router-hono-server/http";
import { createHonoServer } from "react-router-hono-server/node";
export default await createHonoServer({
beforeAll(app) {
app.use(async (c, next) => {
if (c.req.path.includes("/protected") && !c.req.header("Authorization")) {
return reactRouterRedirect("/login");
}
return next();
});
},
});
Using remix-hono middleware
It is easy to use remix-hono middleware with this package.
import { createCookieSessionStorage } from "react-router";
import { createHonoServer } from "react-router-hono-server/node";
import { session } from "remix-hono/session";
export default await createHonoServer({
configure: (server) => {
server.use(
session({
autoCommit: true,
createSessionStorage() {
const sessionStorage = createCookieSessionStorage({
cookie: {
name: "session",
httpOnly: true,
path: "/",
sameSite: "lax",
secrets: [process.env.SESSION_SECRET],
secure: process.env.NODE_ENV === "production",
},
});
return {
...sessionStorage,
// If a user doesn't come back to the app within 30 days, their session will be deleted.
async commitSession(session) {
return sessionStorage.commitSession(session, {
maxAge: 60 * 60 * 24 * 30, // 30 days
});
},
};
},
})
);
},
});
Creating custom Hono Middleware
You can create middleware using the createMiddleware
or createFactory
functions from hono/factory
.
Then, use them with the configure
function of createHonoServer
.
import { createMiddleware } from "hono/factory";
import { createHonoServer } from "react-router-hono-server/node";
export default await createHonoServer({
configure: (server) => {
server.use(
createMiddleware(async (c, next) => {
console.log("middleware");
return next();
})
);
},
});
Using React Router Middleware (unstable future)
You can use React Router middleware with this package.
!TIP Check this example to see how to use it.
!IMPORTANT Please note that this is an unstable feature and could change in the future.
If you already have a custom
getLoadContext
function, you now have to return aMap
from it. Check the example for more information.
Using WebSockets
Node
This package has a built-in helper to use @hono/node-ws
import type { WSContext } from "hono/ws";
import { createHonoServer } from "react-router-hono-server/node";
// Store connected clients
const clients = new Set<WSContext>();
export default await createHonoServer({
useWebSocket: true,
// š Unlock this š from @hono/node-ws
configure: (app, { upgradeWebSocket }) => {
app.get(
"/ws",
upgradeWebSocket((c) => ({
// https://hono.dev/helpers/websocket
onOpen(_, ws) {
console.log("New connection ā¬ļø");
clients.add(ws);
},
onMessage(event, ws) {
console.log("Context", c.req.header("Cookie"));
console.log("Event", event);
console.log(`Message from client: ${event.data}`);
// Broadcast to all clients except sender
clients.forEach((client) => {
if (client.readyState === 1) {
client.send(`${event.data}`);
}
});
},
onClose(_, ws) {
console.log("Connection closed");
clients.delete(ws);
},
}))
);
},
});
Bun
This package has a built-in helper to use hono/bun
in prod and @hono/node-ws
in dev
import { WSContext } from "hono/ws";
import { createHonoServer } from "react-router-hono-server/bun";
// Store connected clients
const clients = new Set<WSContext>();
export default await createHonoServer({
useWebSocket: true,
// š Unlock this š from @hono/node-ws in dev, hono/bun in prod
configure(app, { upgradeWebSocket }) {
app.get(
"/ws",
upgradeWebSocket((c) => ({
// https://hono.dev/helpers/websocket
onOpen(_, ws) {
console.log("New connection š„");
clients.add(ws);
},
onMessage(event, ws) {
console.log("Context", c.req.header("Cookie"));
console.log("Event", event);
console.log(`Message from client: ${event.data}`);
// Broadcast to all clients except sender
clients.forEach((client) => {
if (client.readyState === 1) {
client.send(`${event.data}`);
}
});
},
onClose(_, ws) {
console.log("Connection closed");
clients.delete(ws);
},
}))
);
},
});
Cloudflare Workers
Cloudflare requires a different approach to WebSockets, based on Durable Objects.
!TIP Check this example to see how to use it.
!IMPORTANT For now, HMR is not supported in Cloudflare Workers. Will try to come back to it later.
Work in progress on Cloudflare team: https://github.com/flarelabs-net/vite-plugin-cloudflare
AWS
Not supported.
Basename and Hono sub apps
!NOTE By default, the React Router app is mounted at
/
(defaultbasename
value).You may not need to use this option. It's for advanced use cases.
You can use the basename
option in your React Router config (react-router.config.ts
) to mount your React Router app on a subpath.
It will automatically mount the app on the subpath.
// react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
basename: "/app", // Now the React Router app will be mounted on /app
} satisfies Config;
Then, you can use the app
option in createHonoServer
to pass your "root" Hono app. This will be used to mount the React Router app on the basename
path.
import { Hono } from "hono";
import { createHonoServer } from "react-router-hono-server/node";
import { API_BASENAME, api } from "./api";
import { getLoadContext } from "./context";
// Create a root Hono app
const app = new Hono();
// Mount the API app at /api
app.route(API_BASENAME, api);
export default await createHonoServer({
// Pass the root Hono app to the server.
// It will be used to mount the React Router app on the `basename` defined in react-router.config.ts
app,
getLoadContext,
});
!NOTE You now have two entry points!
/api
- for your API/app
- for your React Router app
Pre-rendering
You should be able to use pre-rendering with this package.
Node and Bun
!TIP Check this example to see how to use it.
!IMPORTANT You need to add the
serverBuildFile
option to yourreact-router.config.ts
file.The file path is fixed to
assets/server-build.js
.
Add the pre-render option to your react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
serverBuildFile: "assets/server-build.js", // šØ Dont forget this
prerender: ["/"],
} satisfies Config;
Cloudflare
Add the pre-render option to your react-router.config.ts
import type { Config } from "@react-router/dev/config";
export default {
prerender: ["/"],
} satisfies Config;
Migrate from v1
You should not expect any breaking changes.
Install the latest version
npm install react-router-hono-server@latest
Create the server file
Option 1 - You previously had all your server code in app/entry.server.tsx
touch app/server.ts
or
npx react-router-hono-server reveal file
Option 2 - You previously had your server code in a server
folder
mkdir app/server
touch app/server/index.ts
or
npx react-router-hono-server reveal folder
Move your server code
Move your previous server code to the new file you created in the previous step.
!NOTE You can remove the import from
react-router-hono-server/node
in yourentry.server.tsx
file and any other server code.
Many options are gone, serverBuildFile
assetsDir
and buildDirectory
.
We now use the Vite virtual import virtual:react-router/server-build
to load the server build and we read the Vite config thanks to the reactRouterHonoServer
plugin.
!IMPORTANT You now need to export the server created by
createHonoServer
as default.import { createHonoServer } from "react-router-hono-server/node"; export default await createHonoServer({/* other options */});
Update your vite.config.ts
!IMPORTANT
devServer
is nowreactRouterHonoServer
.
Many options are gone or have changed.
exportName
(reactRouterHonoServer
expects a default export from your server file), entry
is now serverEntryPoint
. appDirectory
is removed (read from vite.config.ts
), and exclude
has been moved under dev
.
You used buildEnd
from remix()
plugin or a custom buildDirectory
option
You may know that it has been moved to react-router.config.ts
(see here for more information).
If you used this hook for Sentry, check this example to see how to migrate.
If you used a custom buildDirectory
option, check this example to see how to migrate.
Update your package.json scripts
"scripts": {
"build": "react-router build",
"dev": "react-router dev",
"start": "node ./build/server/index.js",
},
Special Thanks
Inspired by remix-express-vite-plugin from @kiliman
remix
handler was forked from remix-hono by @sergiodxa as it is a small and simple core dependency of this library.
I will still help maintain it.
Contributors āØ
This project follows the all-contributors specification. Contributions of any kind welcome!
7 months ago
7 months ago
7 months ago
3 months ago
6 months ago
6 months ago
7 months ago
6 months ago
7 months ago
5 months ago
4 months ago
6 months ago
5 months ago
5 months ago
5 months ago
4 months ago
5 months ago
5 months ago
5 months ago
5 months ago
5 months ago
7 months ago
7 months ago
4 months ago
5 months ago
5 months ago
4 months ago
4 months ago
7 months ago
8 months ago
8 months ago
9 months ago
10 months ago
10 months ago
10 months ago
11 months ago
11 months ago