A REST API is a public contract. Once clients depend on it — mobile apps, third-party integrations, your own frontend — breaking changes are expensive to fix. Good API design is not pedantic rule-following; it's the investment that lets you iterate on your backend without breaking things downstream. These are the practices that consistently produce APIs that age well.
What changed in 2026
- OpenAPI 3.1 is the documentation standard. Auto-generating docs from code annotations (FastAPI, Hono, tRPC) is expected on new APIs.
- API keys are table stakes — but short-lived JWTs with refresh tokens are the standard for user-facing auth.
- Rate limiting is not optional. Public APIs without limits are scraped, abused, or DDoS'd; every cloud provider offers rate-limiting middleware.
- JSON:API and HATEOAS lost. Simpler, pragmatic REST won. The industry settled on opinionated simplicity over strict adherence to any spec.
URL structure
URLs identify resources. HTTP methods express what to do with them.
| Pattern |
Correct |
Incorrect |
| List users |
GET /users |
GET /getUsers |
| Get one user |
GET /users/42 |
GET /user?id=42 |
| Create user |
POST /users |
POST /createUser |
| Update user |
PATCH /users/42 |
POST /updateUser/42 |
| Delete user |
DELETE /users/42 |
GET /deleteUser?id=42 |
| User's orders |
GET /users/42/orders |
GET /getUserOrders?id=42 |
Rules:
- Use nouns (plural) for resources.
- Use HTTP methods to express action.
- Keep URLs lowercase, use hyphens not underscores for multi-word resources.
- Nest resource relationships up to one level; deeper nesting gets unwieldy.
HTTP status codes
Use them correctly — they're how clients know what happened without parsing response bodies.
| Code |
Meaning |
When to use |
| 200 OK |
Success |
GET, PUT, PATCH with response body |
| 201 Created |
Resource created |
POST that creates something |
| 204 No Content |
Success, no body |
DELETE, PUT with no response body |
| 400 Bad Request |
Client sent bad data |
Validation errors |
| 401 Unauthorized |
Not authenticated |
Missing or invalid token |
| 403 Forbidden |
Authenticated but not allowed |
Token valid, insufficient permission |
| 404 Not Found |
Resource doesn't exist |
No resource at that URL |
| 409 Conflict |
State conflict |
Duplicate email, version conflict |
| 422 Unprocessable |
Semantically invalid |
Valid JSON but fails business rules |
| 429 Too Many Requests |
Rate limited |
Exceeded rate limit |
| 500 Internal Server Error |
Server fault |
Unexpected crash; don't expose internals |
Error response format
Pick a format and use it consistently across all endpoints:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email address is already registered.",
"field": "email",
"request_id": "req_8f3a1b2c"
}
}
Include request_id — it lets clients report bugs and lets you find the log entry instantly.
Versioning
Version your API from the first public release. The two practical options:
# URL path versioning (recommended — explicit, cache-friendly)
https://api.example.com/v1/users
# Header versioning (cleaner URLs, harder to test in browser)
GET /users
API-Version: 2026-06-01
URL path versioning is the pragmatic default. Ship v1 even if it feels premature — you'll want it when you introduce breaking changes.
Authentication
| Method |
Use for |
| API key (header) |
Server-to-server, developer APIs |
| JWT (Bearer token) |
User sessions, short-lived (15 min expiry + refresh) |
| OAuth 2.0 |
Delegated access ("Login with Google") |
| mTLS |
Internal microservices, high-security APIs |
# API key (send in header, not query string)
Authorization: Bearer sk_live_abc123...
# Never in the URL — it ends up in server logs:
# https://api.example.com/data?api_key=secret ← Wrong
Pagination
Never return unbounded lists. The two standard approaches:
// Cursor-based (preferred for large or frequently-updated datasets)
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTAwfQ==",
"has_more": true
}
}
// Offset-based (simpler, fine for small datasets)
{
"data": [...],
"pagination": {
"total": 847,
"page": 3,
"per_page": 25,
"total_pages": 34
}
}
Rate limiting
Always return rate limit state in headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 743
X-RateLimit-Reset: 1748900400
Retry-After: 60
Return 429 Too Many Requests when the limit is hit. Include Retry-After so clients back off automatically.
Common mistakes
Inconsistent naming. Mixing camelCase and snake_case in the same API confuses every client. Pick one (JSON convention is camelCase; Python convention leans snake_case) and stick to it.
Exposing database IDs directly. Auto-increment integers reveal how many records you have. Use UUIDs or cuid2 for public-facing IDs.
Returning 200 for errors. Some APIs return {"success": false} with HTTP 200. This forces every client to parse the body before knowing if the call succeeded. Use the HTTP status code.
Not validating input server-side. Client-side validation is UX. Server-side validation is security. Never skip it.
What to skip
- HATEOAS in most APIs — the idea of embedding hypermedia links in every response is sound in theory; in practice almost no client uses them, and they add payload weight without value.
- Custom auth schemes — use standard JWT, API keys, or OAuth; rolling your own authentication is a security liability.
- Exposing internal error details in 500 responses — log the full error server-side, return only a
request_id to the client.
FAQ
Should I use REST or GraphQL?
REST for simple, public APIs with defined use cases. GraphQL when clients need flexible querying over complex, interconnected data (think: GitHub API, Shopify storefront). See What is an API in 2026.
When should I break a version?
When you change a response shape, remove a field, rename a resource, or change authentication requirements. Adding new optional fields is backwards-compatible and doesn't require a new version.
Should I document my API before or while building?
Ideally both — define the API contract (OpenAPI spec) first, then implement. FastAPI and similar frameworks generate the spec from code annotations automatically.
How do I handle bulk operations?
Accept an array in the request body: POST /users/bulk with [{...}, {...}]. Process asynchronously for large batches and return a job ID the client can poll.
Where to go next
See What is an API in 2026, How to build a website in 2026, and How to deploy a web app in 2026.