A REST API is a contract. Once you publish it, clients depend on it, and breaking changes are expensive. The teams that get this wrong don't lack competence — they lack discipline at the design stage, when everything still feels negotiable.
Here's what separates REST APIs that age well from ones that become support tickets.
Resource naming is not optional
Your URL structure is your domain model made public. Get it wrong and you'll spend years working around it.
Nouns, not verbs. The HTTP method is the verb.
# Wrong
POST /createUser
GET /getUserById?id=42
# Right
POST /users
GET /users/42
Plural, consistently. Always /users, never sometimes /user. Consistency matters more than the specific convention you choose.
Hierarchy only when it reflects true ownership. /orders/99/items is legitimate if items can't exist outside an order. But /users/42/orders should be /orders?userId=42 — orders exist independently of users.
Flat beats deeply nested. Three levels deep is already a smell. Four is a problem.
# Avoid
GET /customers/42/accounts/7/transactions/100
# Prefer
GET /transactions/100
HTTP semantics are load-bearing
HTTP methods carry meaning that clients, proxies, and caches rely on. Abusing them creates invisible bugs.
| Method | Semantics | Safe | Idempotent | |--------|-----------|------|------------| | GET | Read | Yes | Yes | | POST | Create / trigger action | No | No | | PUT | Replace entirely | No | Yes | | PATCH | Partial update | No | No | | DELETE | Remove | No | Yes |
Idempotency matters in practice. A DELETE called twice should return 200 the first time and 404 the second — not a 500 error. Network retries happen. Design for them.
Use the right status codes. The status code is part of your API's documentation. 200 OK for everything is lying to your clients.
201 Created — resource was created, include Location header
204 No Content — success with no response body (DELETE, PUT)
400 Bad Request — client sent invalid data; explain what's wrong
401 Unauthorized — authentication required or failed
403 Forbidden — authenticated but not allowed
404 Not Found — resource doesn't exist
409 Conflict — state conflict (duplicate, version mismatch)
422 Unprocessable — validation failed
429 Too Many Reqs — rate limit hit
500 Server Error — your fault, not the client's
Versioning: decide before you ship
There is no perfect versioning strategy. There is only the one you pick before clients exist, versus the one you bolt on after.
URL versioning (/v1/users) is the most explicit and the most common. It's easy to route, easy to deprecate, and easy to test.
Header versioning (Accept: application/vnd.api+json;version=2) is cleaner architecturally but harder to debug, bookmark, and cache.
The rule: whatever you choose, commit to it. A mix of /v1/orders and /orders with an Api-Version header is the worst of both worlds.
Semantic versioning doesn't map well to APIs. Instead, version on breaking changes only:
- Removing or renaming a field
- Changing a field's type
- Making a previously optional field required
- Changing URL structure or method semantics
Adding new optional fields, new endpoints, and new status codes are non-breaking changes.
Pagination, filtering, and sorting
Never return unbounded collections. A /users endpoint that returns 50,000 records will work fine in staging and take down production.
Cursor-based pagination scales better than offset for large datasets and real-time data:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTAwfQ==",
"has_more": true
}
}
Offset pagination is simpler and fine when data volume is bounded:
GET /orders?page=3&per_page=25
Filtering via query params — keep it consistent:
GET /orders?status=pending&created_after=2026-01-01
Sorting:
GET /products?sort=price&order=asc
Document the defaults. Clients shouldn't have to guess what order GET /users returns in.
Error responses need a contract too
A 400 status code without a body is useless. Your error response format should be as consistent as your success response format.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "age",
"message": "Must be at least 18"
}
],
"request_id": "req_abc123"
}
}
Include request_id in every response, success or failure. It makes support conversations possible.
Authentication and authorization patterns
Use Bearer tokens, not API keys in query params. Keys in URLs end up in server logs, browser history, and Slack pastes. Put credentials in headers.
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Separate authentication from authorization. 401 means "I don't know who you are." 403 means "I know who you are and you can't do this." Teams that use only 401 make debugging authorization issues miserable.
Scope your tokens. A token that can do everything is a liability. Design token scopes before you need them, not after a security incident.
The consistency principle
The most important API design principle isn't REST orthodoxy — it's internal consistency. Pick conventions and apply them everywhere:
- Date formats (always ISO 8601:
2026-04-28T10:00:00Z) - ID formats (all integers, or all UUIDs — never a mix)
- Field naming (snake_case or camelCase — not both)
- Boolean naming (
is_active,has_access— notactiveandhasAccess) - Empty vs null vs absent (define the difference and document it)
Clients integrate against your API once. They maintain that integration forever. Inconsistency compounds the cost of every future change.
Document as you go, not after
API documentation written after the fact describes what was built, not what was intended. The best teams use OpenAPI specs as a design artifact — write the spec before writing the handlers, review it with stakeholders, and generate documentation from it automatically.
The spec is also a contract. When it drifts from the implementation, you've lost the single source of truth that makes everything else possible.
The APIs that teams are still happily using five years later aren't the most technically sophisticated ones. They're the consistent ones — the ones where every endpoint behaves predictably, every error is informative, and every breaking change was intentional.
Design for the client who hasn't read your internal docs, because eventually, that's everyone.
If you're building or auditing an API and want a second opinion on the design, let's talk.