Skip to content
UsageRail
Frameworks / Fastify

Fastify plugin

The Fastify plugin checks each request's API key, rate limit and quota in an onRequest hook, before your handler runs, and counts the request when the response is sent.

bash
npm install @usagerail/node
server.ts
import Fastify from "fastify";
import { usageRail } from "@usagerail/node/fastify";

const app = Fastify();

await app.register(usageRail({
  appKey: process.env.USAGERAIL_APP_KEY!,
  auth: "api-key",
}));

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

await app.listen({ port: 3000 });

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

Where to register it

  • Register the plugin before your routes. It skips Fastify's encapsulation, the way fastify-plugin does, so its hook covers the whole app, including routes registered inside other plugins.
  • Fastify has already matched the route when the hook runs, so usage is counted under that route's template, including any register prefix, such as /v1/items/:id. A request no route matched is recorded as _unmatched.

Options

OptionDefaultWhat it does
appKeyRequiredYour app key from the console.
authRequired"api-key" or "custom".
consumerNoneWith auth: "custom", a function that gets the Fastify request and returns { id }, or null for anonymous traffic.
onRejectJSON error(rejection, request, reply) => void to send your own response when a request is refused.
onKeyStoreUnavailable, staleKeySnapshot, onQuotaUnavailableApp settingsOverride the app's availability settings; see the Express page.
snapshottrueSave the synced key list on disk so a restart doesn't wait for the first sync.
flushOnExittrueSend the remaining counts when the process gets SIGTERM or exits.
loggerconsoleWhere warnings go.

Count units instead of requests

In apps that count units, call request.usageRail.track(n) in the handler with a whole number from 0 to 10,000,000. request.usageRail is typed and always set in handlers, because refused requests never reach them. Route weights and reservations work as described under quotas.

Use your own authentication

Fastify runs onRequest hooks in the order they were added, so add your authentication hook before registering UsageRail:

ts
app.addHook("onRequest", authenticate); // sets request.user

await app.register(usageRail({
  appKey: process.env.USAGERAIL_APP_KEY!,
  auth: "custom",
  consumer: (request) => (request.user ? { id: request.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 statuses and JSON body listed on the Express page. To send your own body, use onReject and keep the headers:

ts
await app.register(usageRail({
  appKey: process.env.USAGERAIL_APP_KEY!,
  auth: "api-key",
  onReject: (rejection, request, reply) => {
    reply.code(rejection.status).headers(rejection.headers).send({ code: rejection.error });
  },
}));

What gets counted

  • 2xx and 3xx responses are billable. 4xx and 5xx responses and refused requests are recorded but not billed.
  • A client that disconnects before the response starts is recorded as aborted and isn't billed. If it disconnects while an earlier hook, such as your own authentication, is still running, your handler doesn't run.
  • A server-sent events response counts once, when it starts, and units you pass to track() are added when it ends.
  • With @fastify/websocket, a successful handshake counts once, and units you pass to track() are added when the connection closes. A refused handshake gets the rejection status and the socket is closed.

Running in production

On SIGTERM the plugin sends the remaining counts, waiting at most 5 seconds. If your app handles SIGTERM itself, end your shutdown with await plugin.engine.close(), where plugin is the value usageRail() returned. Counts that haven't been sent when a process is killed outright are lost and never billed.