Skip to content
UsageRail
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.

bash
npm install @usagerail/node
server.js
import 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/express before 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

OptionDefaultWhat it does
appKeyRequiredYour app key from the console. The SDK uses it to sync and to report usage.
authRequired"api-key" to check keys issued by UsageRail, or "custom" to identify callers yourself.
consumerNoneWith auth: "custom", a function that gets req and returns { id } for the caller, or null for anonymous traffic.
onRejectJSON error(rejection, req, res) => void to send your own response when a request is refused.
onKeyStoreUnavailableApp setting"deny" or "allow": what to do with an unknown key while UsageRail can't be reached.
staleKeySnapshotApp setting"allow" or "deny": whether to ask about unknown keys after 10 minutes without a sync.
onQuotaUnavailableApp setting"allow" or "deny": what to do when a quota can't be checked.
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. 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), call req.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:

js
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:

StatuserrorWhen
401invalid_api_keyThe key is missing, malformed, unknown, revoked or expired.
402insufficient_creditsThe customer's prepaid credit is used up.
403consumer_suspendedYou suspended the customer in the console.
429rate_limitedThe customer is over their per-second or per-minute limit.
429quota_exceededThe customer used up this period's quota on a plan that stops at the limit.
503auth_unavailableUsageRail can't be reached, the key isn't in the local copy, and unknown keys are set to be refused.
503quota_unavailableThe 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:

js
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 to track() 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 with app.use()) are grouped by their concrete path, because Express doesn't record where a sub-app is mounted reliably. Use express.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: false to 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(), where middleware is the value usageRail() returned.
  • Hard kills. If the process is killed with kill -9 or runs out of memory, counts it hadn't sent yet are lost and never billed.