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.
npm install @usagerail/nodeimport 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-plugindoes, 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
registerprefix, such as/v1/items/:id. A request no route matched is recorded as_unmatched.
Options
| Option | Default | What it does |
|---|---|---|
appKey | Required | Your app key from the console. |
auth | Required | "api-key" or "custom". |
consumer | None | With auth: "custom", a function that gets the Fastify request and returns { id }, or null for anonymous traffic. |
onReject | JSON error | (rejection, request, reply) => void to send your own response when a request is refused. |
onKeyStoreUnavailable, staleKeySnapshot, onQuotaUnavailable | App settings | Override the app's availability settings; see the Express page. |
snapshot | true | Save the synced key list on disk so a restart doesn't wait for the first sync. |
flushOnExit | true | Send the remaining counts when the process gets SIGTERM or exits. |
logger | console | Where 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:
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:
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 totrack()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.