Skip to content

rphlmr/react-router-hono-server

Repository files navigation

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 and Cloudflare Workers 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 from hono, you may need to install hono 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

Tip

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(), // add this
    reactRouter(),
    tsconfigPaths()
  ],
});

Bun

Tip

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

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

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

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";

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)[];
  };
};

createHonoServer

All adapters
export type HonoServerOptions<E extends Env = BlankEnv> = {
    /**
   * Hono app to use
   *
   * {@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;
  /**
   * Listening listener (production mode only)
   *
   * It is called when the server is listening
   *
   * Defaults log the port
   */
  listeningListener?: (info: AddressInfo) => 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 }
}
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;
}
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"> {}

Middleware

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.

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 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 WebSockets

Node

This package has a built-in helper to use @hono/node-ws

Tip

Check this example to see how to use it.

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

Tip

Check this example to see how to use it.

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

Pre-rendering

You should be able to use pre-rendering with this package.

Tip

Check this example to see how to use it.

Important

You need to add the serverBuildFile option to your react-router.config.ts file.

The file path is fixed to assets/server-build.js.

Add the prerender 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;

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 your entry.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 now reactRouterHonoServer.

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!