A practical hardening checklist for production APIs: role checks on every route, rate limits that bite, and audit trails you can actually query.
By Kibet Brian
API security fails in the boring places: a route that forgot its role check, a login form with no rate limit, an incident with no logs. This is the baseline I apply to every production API before it ships.
Check roles at the route boundary, not deep in handlers where it is easy to forget. A single withRole wrapper keeps the policy visible and makes unaudited routes impossible to add by accident.
1export function withRole(roles: Role[], handler: Handler): Handler {2 return async (req) => {3 const session = await getSession(req);4 if (!session || !roles.includes(session.role)) {5 return Response.json({ error: 'Forbidden' }, { status: 403 });6 }7 return handler(req, session);8 };9}
Limit auth attempts, password resets, and expensive endpoints per IP and per account. Return 429 with a Retry-After header, and alert on sustained limit hits, that pattern is usually an attack or a broken client.
Prefer short-lived tokens and rotate signing secrets without downtime by accepting the previous secret during a grace window. Long-lived API keys belong in a vault with per-key scopes and an owner, never in chat logs or client bundles.
Authentication says who is calling, authorization on the data says what they may touch. Filter every query by the caller's tenant and id, reject unexpected fields instead of silently ignoring them, and test with a second account trying to read the first account's records.
Security is a checklist you run on every route, not a feature you add at the end. Boring, consistent enforcement beats clever one-off protections every time.