REST API design: the decisions that make an API pleasant to use
An API is a product with developers as users. Most of what makes one good is consistency, not cleverness.
Secure an API by authenticating every request, authorising every object access against the caller's identity, validating input against a schema, limiting what each response exposes, rate limiting per identity, and logging enough to detect abuse. Broken object-level authorisation is the most common and most serious API vulnerability.
An endpoint that returns or modifies a resource based on an id supplied by the caller, without checking that the caller may access that resource.
// Vulnerable: any authenticated caller can read any order
app.get("/orders/:id", requireAuth, async (req, res) => {
res.json(await db.order.findUnique({ where: { id: req.params.id } }));
});
// Correct: scope the query to the caller's organisation
app.get("/orders/:id", requireAuth, async (req, res) => {
const order = await db.order.findFirst({
where: { id: req.params.id, organisationId: req.user.organisationId },
});
if (!order) return res.status(404).end();
res.json(order);
});| Method | Good for | Watch out for |
|---|---|---|
| Session cookie | First-party browser clients | Needs CSRF consideration |
| Bearer token (JWT) | Mobile apps, service-to-service | Revocation; verify signature and claims |
| API key | Server-to-server integrations | Rotation, scoping, transmission in headers |
| OAuth 2.1 + PKCE | Third-party delegated access | Complexity; exact redirect matching |
| mTLS | High-trust internal services | Certificate lifecycle management |
// Mass assignment: a caller can set role: "admin"
await db.user.update({ where: { id }, data: req.body });
// Safe: parse to an explicit shape first
const data = updateProfileSchema.parse(req.body); // name, avatarUrl only
await db.user.update({ where: { id }, data });No. They make enumeration impractical but are not authorisation. Ids leak through logs, referrers, shared links and support tickets. Always check ownership.
Ideally yes, with self-service rotation and overlapping validity so rotation causes no downtime. At minimum, make revocation immediate and visible.
Authenticate everything, scope keys narrowly, rate limit aggressively per key, document limits clearly, and monitor for anomalous usage per account.
It has a different risk profile: flexible queries mean depth and complexity attacks, and per-field authorisation is easy to get wrong. Neither is inherently less secure; GraphQL requires more deliberate hardening.
ROVQIX Engineering
Engineering team, ROVQIX
The ROVQIX engineering team builds and maintains web platforms, APIs and infrastructure for clients across SaaS, ecommerce and enterprise. These notes come out of real production work — deploys, incidents, migrations and audits.
ROVQIXdesigns and builds production web platforms — Next.js front ends, Node.js APIs and the infrastructure behind them. Tell us what you're building and we'll scope it with you.
An API is a product with developers as users. Most of what makes one good is consistency, not cleverness.
Rate limiting is not just abuse prevention. It is the mechanism that stops one client's bad afternoon from becoming everyone's outage.
The sessions-versus-JWT argument is really an argument about revocation. Decide how fast you need to be able to log someone out.
No spam. Just the occasional case study and craft breakdown.