npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-router-hono-server

v4.1.3

Published

The Vite plugin you need to create a Hono server for your React Router app in less than 10 seconds.

Readme

React Router Hono Server

Latest compatible runtimes

Run a React Router framework-mode application on Hono—without giving up the runtime you want.

react-router-hono-server provides a Vite plugin and production adapters for Node.js, Bun, Deno, Cloudflare Workers, and AWS Lambda.

It gives your application one clear server boundary:

  • React Router owns routes, loaders, actions, and rendering.
  • Hono owns middleware, API routes, request context, and runtime integration.
  • This package connects them and handles startup, static assets, and build output.

Why use it?

  • One server API across five runtimes. Move between runtimes without redesigning the application.
  • First-class Hono customization. Add middleware, API endpoints, typed context, and runtime options around React Router.
  • Production and development parity. Use the same server entry in Vite development and in the production runtime.
  • React Router features stay intact. SSR, prerendering, SPA output, basenames, and custom build layouts remain React Router configuration.
  • Small default surface. Start with a virtual server entry and reveal files only when customization is needed.

Contents

Runtime matrix

| Runtime | Development | Production | WebSockets | Static assets | | ------------------ | ----------------------------------- | ------------------------------------ | ---------- | ------------------------------- | | Node.js | React Router dev server | Node HTTP/HTTPS | Yes | Node filesystem | | Bun | Bun-powered React Router dev server | Bun.serve | Yes | Bun filesystem | | Deno | Deno-powered Vite dev server | Deno.serve | Yes | Deno filesystem | | Cloudflare Workers | Cloudflare Vite plugin and Workerd | Worker + asset binding | Yes | Workers assets | | AWS Lambda | React Router dev server | Lambda handler or response streaming | No | Use CloudFront/S3 in production |

Choose the runtime that matches your deployment target. Application routes and Hono configuration remain portable; runtime-specific server options stay isolated in app/server.ts.

Requirements and compatibility

Supported versions

  • Node.js 24.19 or newer is required for installation, builds, and the CLI.
  • React 19.2, React DOM 19.2, React Router 8.3 or newer, Vite 8, and Hono 4 are supported.
  • @hono/node-server 2 is installed with this package for Node-backed functionality; applications only need to install it directly when they import it themselves.
  • Bun 1.4 or newer is required for Bun execution.
  • Deno 2 is required for Deno execution.
  • Cloudflare projects require the current @cloudflare/vite-plugin, Wrangler 4, an ASSETS binding, and the nodejs_compat compatibility flag.

[!IMPORTANT] Keep the application and this package on one installation of React, React DOM, React Router, Hono, and Vite.

Aliased or duplicated framework packages can cause invalid hooks, incompatible contexts, and build failures.

Minimal Node quick start

This guide assumes you already have the default React Router framework-mode project. This package adds a Hono server to that application; it does not scaffold the React Router application itself.

The install commands below are changes to that default project. Keep its existing @react-router/dev and vite packages for every runtime. Keep @react-router/node for Node and AWS, but remove it for Bun, Deno, and Cloudflare so React Router selects its Web Streams renderer. This package replaces @react-router/serve on every runtime.

The following setup creates a Node.js server. The other runtime guides use the same structure with a different adapter.

1. Install the packages

pnpm remove @react-router/serve
pnpm add react-router-hono-server hono

2. Add the Vite plugin

Create vite.config.ts. The Hono server plugin must come before reactRouter().

import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouterHonoServer(), reactRouter()],
});

3. Create the server entry

Create app/server.ts:

import { createHonoServer } from "react-router-hono-server/node";

export default await createHonoServer();

4. Add package scripts

Use these scripts in package.json:

{
  "scripts": {
    "build": "react-router build",
    "dev": "react-router dev",
    "start": "node ./build/server/index.js",
    "typecheck": "react-router typegen && tsc --noEmit"
  }
}

5. Run the application

# Development
pnpm dev

# Production
pnpm build
pnpm start

Runtime selection

The runtime is selected in two places:

  1. Set runtime in reactRouterHonoServer().
  2. Import the matching adapter from the server entry.

| Runtime | Plugin option | Server import | | ---------- | ----------------- | ------------------------------------- | | Node.js | omitted or node | react-router-hono-server/node | | Bun | bun | react-router-hono-server/bun | | Deno | deno | react-router-hono-server/deno | | Cloudflare | cloudflare | react-router-hono-server/cloudflare | | AWS Lambda | aws | react-router-hono-server/aws-lambda |

[!IMPORTANT] The plugin must precede reactRouter(). On Cloudflare, cloudflare() must precede both plugins.

Reveal and entry files

React Router and this package each provide a default entry file. These entries operate at different layers and solve different problems.

| Entry | Owned by | Responsibility | | ---------------------- | -------------------------- | ---------------------------------------------------------------------- | | app/server.ts | react-router-hono-server | Hono middleware, API routes, load context, assets, and runtime startup | | app/entry.server.tsx | React Router | Rendering a matched React Router request into a response | | app/entry.client.tsx | React Router | Browser hydration |

Reveal only the entry you need to customize.

Hono server entry

Without app/server.ts or app/server/index.ts, the plugin supplies a virtual Hono server with default options.

Reveal the Hono entry when you need any of the following:

  • Hono middleware or API routes
  • A React Router load context
  • WebSockets
  • Static-file configuration
  • Runtime-specific server options

Create app/server.ts:

npx react-router-hono-server reveal file

Use the folder form when the server has colocated modules:

npx react-router-hono-server reveal folder

The CLI infers the runtime from vite.config.ts. If it cannot find a runtime option, it generates a Node.js entry.

[!WARNING] Run the command from the project root. The reveal command overwrites its target, so do not run it over an entry you have already customized.

React Router rendering entries

React Router supplies hidden client and server rendering entries. It selects the server renderer from the application's dependencies:

  • Applications with @react-router/node, @react-router/express, or @react-router/serve use the Node streaming entry.
  • Applications without those packages use the Web Streams entry.
  • A custom app/entry.server.tsx always takes precedence.

Reveal the entries only when the application needs custom hydration or SSR behavior:

npx react-router reveal

The command generates both app/entry.client.tsx and app/entry.server.tsx.

See the React Router reveal documentation for the generated files.

Optional React Router future flags

React Router 8.3 has no stable future flags. Its two unstable flags are optional and belong in react-router.config.ts; reactRouterHonoServer() does not duplicate or enable them:

import type { Config } from "@react-router/dev/config";

export default {
  future: {
    unstable_enableNodeReadableStream: true,
    unstable_optimizeDeps: true,
  },
} satisfies Config;
  • unstable_enableNodeReadableStream makes React Router use its Web Streams default entry on Node. It has no effect when app/entry.server.tsx exists.
  • unstable_optimizeDeps adds the client entry and route modules to Vite's dependency optimizer in development. If it causes optimization issues, remove the flag and restart the dev server.

These flags are experimental and may change in React Router minor releases. See React Router's future changes guide and changelog.

Runtime guides

Node.js

Node.js is the default runtime and the shortest path to production. Follow the minimal Node quick start; no runtime option is required.

Node.js runtime notes

  • React Router's default Node rendering entry is compatible.
  • Set the listening port with PORT or the port server option.
  • Use hostname to control the listening interface.
  • Advanced options include listeningListener, onServe, customNodeServer, and overrideGlobalObjects.
  • Static-file customization and WebSockets are supported.

Bun

Install

bun remove @react-router/node @react-router/serve
bun add react-router-hono-server hono
bun add -d @types/bun

Configure Vite

Create vite.config.ts and select the Bun runtime:

import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouterHonoServer({ runtime: "bun" }), reactRouter()],
});

Create the server entry

Create app/server.ts:

import { createHonoServer } from "react-router-hono-server/bun";

export default await createHonoServer();

Add scripts

{
  "scripts": {
    "build": "bun x --bun react-router build",
    "dev": "bun x --bun vite",
    "start": "bun ./build/server/index.js",
    "typecheck": "react-router typegen && tsc --noEmit"
  }
}

Run

# Development
bun run dev

# Production
bun run build
bun run start

Bun runtime notes

  • bun x --bun react-router build runs React Router and prerendering under Bun instead of following the CLI's Node.js shebang.
  • bun x --bun vite forces Vite and its child processes to run with Bun.
  • React Router automatically uses its Web Streams server entry.
  • customBunServer forwards options to Bun.serve.
  • Graceful shutdown, static-file customization, and WebSockets are supported.

Deno

Install

In package.json, remove @react-router/node and @react-router/serve, add react-router-hono-server and hono to dependencies, then install with Deno:

deno install --allow-scripts --minimum-dependency-age=0

Configure Vite

Create vite.config.ts and select the Deno runtime:

import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouterHonoServer({ runtime: "deno" }), reactRouter()],
});

Create the server entry

Create app/server.ts:

import { createHonoServer } from "react-router-hono-server/deno";

export default await createHonoServer();

Add scripts

{
  "scripts": {
    "build": "react-router build",
    "dev": "deno run --conditions=development --allow-all npm:@react-router/dev dev",
    "start": "deno run --allow-all ./build/server/index.js",
    "typecheck": "react-router typegen && tsc --noEmit"
  }
}

Run

# Development
deno task dev

# Production
deno task build
deno task start

Deno runtime notes

  • The development command enables the development export condition required by React Router.
  • customDenoServer forwards options to Deno.serve.
  • Graceful shutdown and static-file customization are supported.
  • WebSockets use the optional ws peer in Vite development and Deno's native implementation in production.

Cloudflare Workers

Install

pnpm remove @react-router/node @react-router/serve
pnpm add react-router-hono-server hono
pnpm add -D @cloudflare/vite-plugin @cloudflare/workers-types wrangler

Configure Vite

Create vite.config.ts. Plugin order is required:

import { cloudflare } from "@cloudflare/vite-plugin";
import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [
    cloudflare({ viteEnvironment: { name: "ssr" } }),
    reactRouterHonoServer({ runtime: "cloudflare" }),
    reactRouter(),
  ],
});

Create the server entry

Create app/server.ts:

import { createHonoServer } from "react-router-hono-server/cloudflare";

export default await createHonoServer();

Configure Wrangler

Create wrangler.jsonc:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "my-react-router-worker",
  "compatibility_date": "2026-08-11",
  "compatibility_flags": ["nodejs_compat"],
  "main": "./app/server.ts",
  "assets": {
    "directory": "./build/client",
    "binding": "ASSETS",
  },
}

The ASSETS binding connects the generated client directory to the Worker.

Add scripts

{
  "scripts": {
    "build": "react-router build",
    "dev": "vite dev",
    "start": "vite preview",
    "typecheck": "react-router typegen && tsc --noEmit"
  }
}

Run

# Workerd-backed development
pnpm dev

# Local production preview
pnpm build
pnpm start

Deploy the generated Worker with your normal Cloudflare workflow.

Cloudflare runtime notes

  • Public files and generated client files are served through ASSETS.
  • Missing or unsuccessful asset responses fall through to Hono and React Router.
  • Prerendering and SPA mode are supported.
  • With ssr: true, a route without a generated asset falls through to runtime SSR.
  • WebSockets use Cloudflare's native WebSocketPair implementation in both workerd-backed development and production.

AWS Lambda

Install

pnpm remove @react-router/serve
pnpm add react-router-hono-server hono

Configure Vite

Create vite.config.ts and select the AWS runtime:

import { reactRouter } from "@react-router/dev/vite";
import { reactRouterHonoServer } from "react-router-hono-server/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [reactRouterHonoServer({ runtime: "aws" }), reactRouter()],
});

Create the Lambda entry

Create app/server.ts for the default response mode:

import { createHonoServer } from "react-router-hono-server/aws-lambda";

export default await createHonoServer({ invokeMode: "default" });

Set invokeMode: "stream" to use Lambda response streaming.

React Router's default Node rendering entry is compatible. Reveal it only when the application needs custom SSR behavior.

Add scripts

{
  "scripts": {
    "build": "react-router build",
    "dev": "react-router dev",
    "typecheck": "react-router typegen && tsc --noEmit"
  }
}

Build and deploy

Use pnpm dev locally, then run pnpm build before packaging build/server for Lambda.

Your infrastructure must expose the generated default handler as the Lambda entry.

AWS runtime notes

  • Production static files should be served by S3, CloudFront, or another asset service.
  • Prerendered files are generated in build/client; deploy them with the static assets.
  • Requests that reach Lambda continue through runtime SSR.
  • Both default responses and Lambda response streaming are supported.

Server customization

Create or reveal a Hono server entry before using these options:

npx react-router-hono-server reveal file

Hono app and middleware ordering

Pass app to use an existing Hono instance. Hooks execute in this order:

| Order | Hook or middleware | Typical use | | ----- | ----------------------- | ------------------------------------------------------------ | | 1 | beforeAll(app) | Authentication or request policy that must run before assets | | 2 | Built-in asset handling | Public files and generated client assets | | 3 | Built-in logger | Request logging when defaultLogger is enabled | | 4 | configure(app) | API routes and application middleware | | 5 | React Router handler | Loaders, actions, and rendered routes |

import { Hono } from "hono";
import { createHonoServer } from "react-router-hono-server/node";

const app = new Hono();

export default await createHonoServer({
  app,
  defaultLogger: false,
  beforeAll(server) {
    server.use("/private/*", async (c, next) => {
      if (!c.req.header("authorization")) return c.text("Unauthorized", 401);
      await next();
    });
  },
  configure(server) {
    server.get("/api/health", (c) => c.json({ ok: true }));
  },
});

Chrome DevTools automatic workspaces

To let Chrome DevTools automatically connect to your project, create public/.well-known/appspecific/com.chrome.devtools.json with your project's absolute path and a stable UUID v4:

{
  "workspace": {
    "root": "/absolute/path/to/your/project",
    "uuid": "53b029bb-c989-4dca-969b-835fecec3717"
  }
}

The development server serves this file before the React Router handler. When the file is absent, the discovery request returns a quiet 404. See Automatic Workspace connection in Chrome DevTools for setup details and security considerations.

Typed React Router context

React Router 8 always expects getLoadContext to return a RouterContextProvider.

import { createContext, RouterContextProvider } from "react-router";
import { createHonoServer } from "react-router-hono-server/node";

export const requestIdContext = createContext<string>();

export default await createHonoServer({
  getLoadContext(c) {
    const context = new RouterContextProvider();
    context.set(requestIdContext, c.req.header("x-request-id") ?? crypto.randomUUID());
    return context;
  },
});

Each adapter also exports createGetLoadContext for separately declared callbacks that need contextual typing.

WebSockets

Node, Bun, Deno, and Cloudflare Workers support the Hono WebSocket helper in development and production.

[!NOTE] Node always uses @hono/node-server. Bun and Deno use it only while running through Vite in development. Install the optional ws peer and its TypeScript declarations for those modes.

Bun and Deno production builds use their native Hono adapters. They do not load ws in production.

Cloudflare uses its native WebSocketPair implementation in workerd-backed Vite development and production, so it never requires ws.

pnpm add ws
pnpm add -D @types/ws

The portable callback API exposes upgradeWebSocket:

import { createHonoServer } from "react-router-hono-server/deno";

export default await createHonoServer({
  useWebSocket: true,
  configure(app, { upgradeWebSocket }) {
    app.get(
      "/ws",
      upgradeWebSocket(() => ({
        onMessage(event, ws) {
          ws.send(`echo:${event.data}`);
        },
      })),
    );
  },
});

Use the matching /bun, /deno, or /cloudflare adapter import.

Cloudflare's Hono helper does not support onOpen. Use onMessage, onClose, and onError there.

The Node adapter exposes its underlying ws.WebSocketServer as wss:

import { createHonoServer } from "react-router-hono-server/node";

export default await createHonoServer({
  useWebSocket: true,
  configure(app, { upgradeWebSocket, wss }) {
    wss.on("connection", (socket) => {
      socket.send("connected");
    });

    app.get(
      "/ws",
      upgradeWebSocket(() => ({
        onMessage(event, ws) {
          ws.send(`echo:${event.data}`);
        },
      })),
    );
  },
});

wss is Node-only because it is the concrete server created by the ws package. Bun, Deno, and Cloudflare use runtime-native, per-connection WebSocket APIs instead of an equivalent central WebSocketServer.

The temporary wss used by Bun and Deno during Vite development is not exposed. Doing so would provide an API that disappears in production.

Node WebSockets use @hono/node-server 2 and coexist with Vite HMR. Use wss.on("connection"), wss.clients, and the connection sockets' ping() method for server-level connection management and heartbeat handling.

Bun, Deno, and Cloudflare expose only upgradeWebSocket.

Basename and prerendering

Prerendering is configured by React Router in react-router.config.ts.

Prerender every static route

import type { Config } from "@react-router/dev/config";

export default {
  prerender: true,
} satisfies Config;

Dynamic routes are not included because their parameter values are unknown.

Prerender selected routes

import type { Config } from "@react-router/dev/config";

export default {
  prerender: ["/", "/about", "/posts/launch"],
} satisfies Config;

Use concrete paths such as /posts/launch for dynamic routes. Paths are relative to the React Router basename; do not include the basename itself.

Discover routes asynchronously

import type { Config } from "@react-router/dev/config";

export default {
  prerender: {
    async paths() {
      return ["/", "/about"];
    },
    concurrency: 4,
  },
} satisfies Config;

React Router also accepts basename, appDirectory, and buildDirectory in the same configuration file.

React Router basename and Vite base

React Router's basename and Vite's base configure independent URL spaces:

  • basename mounts documents, route data requests, and the React Router handler.
  • base controls Vite development URLs and URLs emitted for generated JavaScript, CSS, and other bundled assets.

For an application deployed entirely beneath /v2, configure both upstream tools:

// react-router.config.ts
export default { basename: "/v2" };

// vite.config.ts
export default defineConfig({ base: "/v2/", plugins: [reactRouterHonoServer(), reactRouter()] });

The prefixes may intentionally differ. For example, basename: "/v2/app" with base: "/v2/" keeps application documents beneath /v2/app while generated assets remain beneath /v2/assets. Likewise, basename: "/" with base: "/v2/" keeps documents at the origin root and moves only Vite-owned URLs.

The Node, Bun, and Deno adapters serve generated assets locally when base is an absolute pathname. Cloudflare Workers passes the original asset URL to its asset binding, and AWS production expects an external asset service. Full-URL bases remain externally owned. Relative bases ("" and "./") are preserved as emitted by Vite and do not create a fixed server-side mount. Public-directory files always remain available from the origin root rather than beneath base.

Deployment behavior

| Configuration | Node, Bun, Deno, and Cloudflare Workers | AWS Lambda | | ---------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | ssr: true with prerender | Generates and serves static documents and route data.Unmatched paths use runtime SSR. | Generates the same static output, but a separate AWS asset service must serve it.Unmatched Lambda requests use SSR. | | ssr: false | Generates static output and an SPA fallback for static hosting | Generates static output and an SPA fallback for static hosting |

  • With ssr: true, generated files are used first and unmatched routes continue to runtime SSR.
  • With ssr: false, React Router emits static output and an SPA fallback for static hosting.
  • AWS generates the same client output, but a separate asset service must serve it before requests reach Lambda.

The integration suite covers static and dynamic paths, async discovery, concurrency, SSR and SPA fallbacks, basenames, and custom application/build directories on every adapter.

See React Router's pre-rendering guide for the full configuration contract.

The plugin reads React Router's resolved configuration and mounts the Hono-backed handler at the same basename.

API and exports

| Export | Purpose | | ------------------------------ | ------------------------------------------------------------------------------------- | | react-router-hono-server/dev | reactRouterHonoServer(options) Vite plugin | | /node | Node createHonoServer, options, and createGetLoadContext | | /bun | Bun createHonoServer, options, and createGetLoadContext | | /deno | Deno createHonoServer, options, and createGetLoadContext | | /cloudflare | Cloudflare createHonoServer, options, and createGetLoadContext | | /aws-lambda | AWS handler factory, options, and createGetLoadContext | | /middleware | cache(seconds) static-response middleware | | /http | redirect(c, location), deprecated reactRouterRedirect(location), and getPath(c) | | CLI | react-router-hono-server reveal file or reveal folder |

Vite plugin options

| Option | Purpose | | ------------------ | --------------------------------------------------------------- | | runtime | Selects the production adapter; defaults to node | | serverEntryPoint | Overrides discovery of app/server.ts or app/server/index.ts | | dev.exclude | Extends the paths excluded from Hono dev-server handling | | dev.export | Selects a named export from the server entry during development |

When no server entry is discovered, the plugin supplies a virtual default server for the selected runtime.

Troubleshooting

| Problem | Resolution | | ------------------------------------------- | ------------------------------------------------------------------------------------ | | Cloudflare plugin is missing | Add cloudflare() before both reactRouterHonoServer() and reactRouter() | | Invalid hook calls or incompatible contexts | Remove aliases and duplicate framework installations, then perform one clean install | | Server entry is not discovered | Create app/server.ts, create app/server/index.ts, or set serverEntryPoint | | Load context fails at runtime | Return a RouterContextProvider, not a plain object | | An asset request returns application HTML | Verify buildDirectory; on Cloudflare, also verify the ASSETS binding | | Upgrading from the previous major | Follow MIGRATION.md and complete its clean-install checklist |