Frameworks / Express
Express middleware
The Express middleware checks each request's API key, rate limit and quota before your route runs, and counts the request when the response is finished.
npm install @usagerail/nodeimport express from "express";
import { usageRail } from "@usagerail/node/express";
const app = express();
app.use(usageRail({
appKey: process.env.USAGERAIL_APP_KEY,
auth: "api-key",
}));
app.post("/v1/generate", express.json(), async (req, res) => {
const result = await generate(req.body);
req.usageRail.track(result.tokens); // optional: count tokens
res.json(result);
});The key comes from the Authorization: Bearer header, or from x-api-key when the request has no bearer token.
Where to put it
- Register the middleware with
app.use()before the routes it should cover. Routes registered before it never reach it, so they aren't checked or counted. - To cover part of your API only, mount it on a path:
app.use("/v1", usageRail({ ... })). - Import
@usagerail/node/expressbefore the files that define your routers. On import, it starts recording the path each router is mounted at, such as/tenants/:tenantId, so usage is grouped by the route you wrote rather than by/tenants/42. Routers mounted before the import are grouped by the concrete path.
Options
| Option | Default | What it does |
|---|---|---|
appKey | Required | Your app key from the console. The SDK uses it to sync and to report usage. |
auth | Required | "api-key" to check keys issued by UsageRail, or "custom" to identify callers yourself. |
consumer | None | With auth: "custom", a function that gets req and returns { id } for the caller, or null for anonymous traffic. |
onReject | JSON error | (rejection, req, res) => void to send your own response when a request is refused. |
onKeyStoreUnavailable | App setting | "deny" or "allow": what to do with an unknown key while UsageRail can't be reached. |
staleKeySnapshot | App setting | "allow" or "deny": whether to ask about unknown keys after 10 minutes without a sync. |
onQuotaUnavailable | App setting | "allow" or "deny": what to do when a quota can't be checked. |
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. Any object with a warn(message) method. |
The three availability options override the app's settings in the console. Leave them out to manage the behaviour from the console instead. API keys and quotas explain each choice.
Count units instead of requests
Every billable request counts as 1 unless you change it. There are two ways to count more:
- Route weights. Under Settings, give a route a weight, such as 10 for
POST /v1/images. Each request to it counts as 10. track(units). If the app counts units (Settings > What counts as usage), callreq.usageRail.track(n)in the handler with a whole number from 0 to 10,000,000. Call it again to replace the number. A billable request that never calls it counts as 1, or as the route's weight.
When an app counts requests, track() does nothing, so you can leave the calls in while you decide.
In TypeScript, req.usageRail is optional on Express's Request type, since routes outside the middleware don't have it. Write req.usageRail?.track(n).
Use your own authentication
If your API already identifies its callers, put UsageRail after your authentication middleware and tell it who the caller is:
app.use(authenticate); // your own middleware, sets req.user
app.use(usageRail({
appKey: process.env.USAGERAIL_APP_KEY,
auth: "custom",
consumer: (req) => (req.user ? { id: req.user.accountId } : null),
}));The first time UsageRail sees an id, it creates a customer for it on the app's default plan. By default the ID is hashed with a secret of your app before it leaves your server, so we never store your raw IDs.
Returning null makes the request anonymous: it's recorded for analytics, but it isn't rate limited, doesn't count against a quota and is never billed.
Rejected requests
When the middleware refuses a request, your route doesn't run and the client gets one of these:
| Status | error | When |
|---|---|---|
| 401 | invalid_api_key | The key is missing, malformed, unknown, revoked or expired. |
| 402 | insufficient_credits | The customer's prepaid credit is used up. |
| 403 | consumer_suspended | You suspended the customer in the console. |
| 429 | rate_limited | The customer is over their per-second or per-minute limit. |
| 429 | quota_exceeded | The customer used up this period's quota on a plan that stops at the limit. |
| 503 | auth_unavailable | UsageRail can't be reached, the key isn't in the local copy, and unknown keys are set to be refused. |
| 503 | quota_unavailable | The quota can't be checked and that's set to be refused. |
The body is JSON, such as {"error": "rate_limited", "retry_after": 1}. On a 429, retry_after and the Retry-After header give the seconds to wait; rate limits lists the other headers.
To send your own body, pass onReject. Keep the status and headers so clients can still back off:
app.use(usageRail({
appKey: process.env.USAGERAIL_APP_KEY,
auth: "api-key",
onReject: (rejection, req, res) => {
res.status(rejection.status).set(rejection.headers).json({
code: rejection.error,
message: "See https://docs.example.com/errors",
});
},
}));What gets counted
- Each request is counted under its method, its route template (the router's mount path plus the route's path, such as
/v1/teams/:teamId/items/:itemId) and its outcome. - Responses with a 2xx or 3xx status are billable. 4xx and 5xx responses, rejected requests, and requests the client abandoned before the response started are recorded but never billed.
- A request that matched no route is recorded as
_unmatched. - A server-sent events response (
text/event-stream) counts once, when it starts. Units you pass totrack()during the stream are added when it ends. - WebSocket upgrades aren't checked or counted.
- Requests handled inside a sub-app (one made with
express()and mounted withapp.use()) are grouped by their concrete path, because Express doesn't record where a sub-app is mounted reliably. Useexpress.Router()where you want usage grouped by route.
Running in production
- Several processes. Each process syncs and reports on its own, so you can run as many as you like. Rate limits are counted per process unless you split them; see rate limits.
- Restarts. The synced key list is saved in a private folder in the system temp directory, so a restarted process can check keys before its first sync. Pass
snapshot: falseto turn this off. - Shutdown. On SIGTERM the SDK sends what's left, waiting at most 5 seconds, then lets the process exit. If your app handles SIGTERM itself, finish your own shutdown with
await middleware.engine.close(), wheremiddlewareis the valueusageRail()returned. - Hard kills. If the process is killed with
kill -9or runs out of memory, counts it hadn't sent yet are lost and never billed.