/react-router-hono-server

Remix with Hono in less than 30 seconds

Primary LanguageTypeScriptMIT LicenseMIT

React Router Hono Server

Inspired by remix-express-vite-plugin from @kiliman

This package contains a helper function that enables you to create your Hono server directly from you entry.server.tsx. Since the Hono server is built along with the rest of your Remix 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 relies on remix-hono and presets a default Hono server config that you can customize

Important

Only works with Remix in ESM mode

Only works with Vite

Only works for node

Tip

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

Configuration

Create the server

From your entry.server.tsx file, export the server from createHonoServer and name it server or the name you defined in devServer({exportName}) in your vite.config.ts.

// app/entry.server.tsx

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

export const server = await createHonoServer();

Alternative

You can create your server in a separate file and export it from your entry.server.tsx.

It is useful if you have many middleware and want to keep your entry.server.tsx clean.

// app/server.ts

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

export const server = await createHonoServer();
// app/entry.server.tsx

export * from "./server";

Add the Vite plugin

// vite.config.ts

import { vitePlugin as remix } from "@remix-run/dev";
import { installGlobals } from "@remix-run/node";
import { devServer } from "react-router-hono-server/dev";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";

installGlobals();

export default defineConfig({
  build: {
    target: "esnext",
  },
  plugins: [devServer(), remix(), tsconfigPaths()],
});

Important

Change the target to esnext in your vite.config.ts file

build: {
  target: "esnext",
},

Update package.json scripts

  "scripts": {
    "build": "NODE_ENV=production remix vite:build",
    "dev": "vite --host",
    "start": "NODE_ENV=production node ./build/server/index.js"
  },

How it works

This helper function works differently depending on the environment.

For development, it creates an Hono server that the Vite plugin will load via viteDevServer.ssrLoadModule('virtual:remix/server-build'). The actual server is controlled by Vite through @hono/vite-dev-server, and can be configured via vite.config.ts server options.

For 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 in the same bundle as the rest of your Remix app, you can import app modules just like you normally would.

To run the server in production, use NODE_ENV=production node ./build/server/index.js.

That's all!

Options

export type HonoServerOptions<E extends Env = BlankEnv> = {
  /**
   * Enable the default logger
   *
   * Defaults to `true`
   */
  defaultLogger?: boolean;
  /**
   * The port to start the server on
   *
   * Defaults to `process.env.PORT || 3000`
   */
  port?: number;
  /**
   * The directory where the server build files are located (defined in vite.config)
   *
   * Defaults to `build/server`
   *
   * See https://remix.run/docs/en/main/file-conventions/vite-config#builddirectory
   */
  buildDirectory?: string;
  /**
   * The file name of the server build file (defined in vite.config)
   *
   * Defaults to `index.js`
   *
   * See https://remix.run/docs/en/main/file-conventions/vite-config#serverbuildfile
   */
  serverBuildFile?: `${string}.js`;
  /**
   * The directory where the assets are located (defined in vite.config, build.assetsDir)
   *
   * Defaults to `assets`
   *
   * See https://vitejs.dev/config/build-options#build-assetsdir
   */
  assetsDir?: string;
  /**
   * Customize the Hono server, for example, adding middleware
   *
   * It is applied after the default middleware and before the remix middleware
   */
  configure?: <E extends Env = BlankEnv>(server: Hono<E>) => Promise<void> | void;
  /**
   * Augment the Remix AppLoadContext
   *
   * Don't forget to declare the AppLoadContext in your app, next to where you create the Hono server
   *
   * ```ts
   * declare module "@remix-run/node" {
   *   interface AppLoadContext {
   *     // Add your custom context here
   *   }
   * }
   * ```
   */
  getLoadContext?: (
    c: Context,
    options: Pick<RemixMiddlewareOptions, "build" | "mode">
  ) => Promise<AppLoadContext> | AppLoadContext;
  /**
   * Listening listener (production mode only)
   *
   * It is called when the server is listening
   *
   * Defaults log the port
   */
  listeningListener?: (info: { port: number }) => void;
  /**
   * Hono constructor options
   *
   * {@link HonoOptions}
   */
  honoOptions?: HonoOptions<E>;
};

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 Remix 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 Remix app, you can import app modules just like you normally would.

Example

// app/entry.server.tsx

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

/**
 * Declare our loaders and actions context type
 */
declare module "@remix-run/node" {
  interface AppLoadContext {
    /**
     * The app version from the build assets
     */
    readonly appVersion: string;
  }
}

export const server = await createHonoServer({
  getLoadContext(_, { build, mode }) {
    const isProductionMode = mode === "production";
    return {
      appVersion: isProductionMode ? build.assets.version : "dev",
    };
  },
});
// app/routes/test.tsx

export async function loader({ context }: LoaderFunctionArgs) {
  // get the context provided from `getLoadContext`
  return { appVersion: context.appVersion }
}

Middleware

Middleware are functions that are called before Remix 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 "@remix-run/node";
import { createHonoServer } from "react-router-hono-server/node";
import { session } from "remix-hono/session";

export const server = 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 const server = await createHonoServer({
  configure: (server) => {
    server.use(
      createMiddleware(async (c, next) => {
        console.log("middleware");
        return next();
      })
    );
  },
});

Contributors ✨

This project follows the all-contributors specification. Contributions of any kind welcome!