Skip to content
UsageRail
Frameworks / Hono

Hono middleware

The Hono middleware checks each request's API key, rate limit and quota before your handler runs, and counts the request once the response is ready. This page covers Hono on Node.js.

bash
npm install @usagerail/node
server.ts
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { usageRail, type UsageRailVariables } from "@usagerail/node/hono";
import { fileSnapshotStore, flushOnExit } from "@usagerail/node/node";

const appKey = process.env.USAGERAIL_APP_KEY!;
const rail = usageRail({
  appKey,
  auth: "api-key",
  snapshotStore: fileSnapshotStore(appKey),
});
flushOnExit(rail.engine);

const app = new Hono<{ Variables: UsageRailVariables }>();
app.use("*", rail);

app.post("/v1/generate", async (c) => {
  const result = await generate(await c.req.json());
  c.get("usageRail").track(result.tokens); // optional: count tokens
  return c.json(result);
});

serve({ fetch: app.fetch, port: 3000 });

The key comes from the Authorization: Bearer header, or from x-api-key when the request has no bearer token.

Process setup

Unlike the Express and Fastify integrations, the Hono middleware doesn't touch your process, because Hono also runs where there's no file system or process to hook into. On Node.js, add the two lines from the example:

  • snapshotStore: fileSnapshotStore(appKey) saves the synced key list in a private folder in the system temp directory, so a restarted process can check keys before its first sync.
  • flushOnExit(rail.engine) sends the remaining counts when the process gets SIGTERM or exits, waiting at most 5 seconds. If your app handles SIGTERM itself, finish your shutdown with await rail.engine.close() instead.

Without flushOnExit, counts that haven't been sent when the process stops are lost and never billed.

Options

usageRail() takes appKey, auth, consumer, onKeyStoreUnavailable, staleKeySnapshot, onQuotaUnavailable and logger, which work as on the Express page, plus:

OptionDefaultWhat it does
onRejectJSON error(rejection, c) => Response to send your own response when a request is refused.
snapshotStoreNoneWhere to save the synced key list. Use fileSnapshotStore(appKey) on Node.js.

Count units instead of requests

In apps that count units, call c.get("usageRail").track(n) in the handler with a whole number from 0 to 10,000,000. Route weights and reservations work as described under quotas. Type the app with UsageRailVariables, as in the example, so c.get("usageRail") is typed.

Use your own authentication

Register UsageRail after the middleware that identifies the caller, and return their ID from consumer:

ts
app.use("*", authenticate); // your own middleware, calls c.set("user", ...)

app.use("*", usageRail({
  appKey,
  auth: "custom",
  consumer: (c) => {
    const user = c.get("user");
    return user ? { id: user.accountId } : null;
  },
}));

Returning null makes the request anonymous: it's recorded, but not rate limited, not counted against a quota and never billed.

Rejected requests

Refused requests get the same statuses and JSON body as with Express; see the table on the Express page. To send your own body, return a response from onReject and keep the headers:

ts
usageRail({
  appKey,
  auth: "api-key",
  onReject: (rejection, c) =>
    c.json({ code: rejection.error }, rejection.status, { ...rejection.headers }),
});

What gets counted

  • Each request is counted under the route template Hono matched, such as /v1/items/:id, its method and its outcome. A request no route handled is recorded as _unmatched.
  • 2xx and 3xx responses are billable. 4xx and 5xx responses and refused requests are recorded but not billed.
  • Hono matches routes case-sensitively, and so does the middleware when it reserves units at the start of a request.
  • A server-sent events response counts once, when it starts. Units you pass to track() during the stream are added when it ends or the client disconnects.
  • A successful WebSocket handshake (a 101 response) counts once.
  • HEAD requests answered by a GET handler count as ordinary requests.