API Design Best Practices: Build Interfaces Developers Actually Want to Use
API Design Best Practices#
A great API disappears. Developers consume it without reading docs, guess endpoints correctly, and handle errors gracefully — because the design makes the right thing obvious.
REST Conventions That Matter#
REST is not a spec; it is a set of constraints. The ones that actually impact developer experience:
Resources are nouns, not verbs.
GET /users ✓ (collection)
GET /users/42 ✓ (single resource)
POST /createUser ✗ (verb in URL)
POST /users ✓ (creation via HTTP method)
Use plural nouns consistently. /users/42 not /user/42. Plurals eliminate ambiguity between collections and singletons.
Use kebab-case for multi-word resources. /order-items not /orderItems or /order_items. URLs are case-insensitive by convention, and kebab-case reads naturally.
HTTP Methods — Use Them Correctly#
Each method has semantics. Respect them:
| Method | Purpose | Idempotent | Safe |
|---|---|---|---|
| GET | Read resource | Yes | Yes |
| POST | Create resource | No | No |
| PUT | Full replace | Yes | No |
| PATCH | Partial update | No* | No |
| DELETE | Remove resource | Yes | No |
Common mistakes:
- Using POST for everything (the "RPC over HTTP" anti-pattern)
- Using PUT for partial updates (PUT means full replacement)
- Not returning 204 for successful DELETE with no body
- Treating DELETE as non-idempotent (deleting twice should not error — return 204 or 404)
Pagination: Cursor vs Offset#
Offset-based — simple, familiar, broken at scale:
GET /users?offset=100&limit=25
Problem: if a record is inserted while paginating, you either skip or duplicate entries. Performance degrades on large offsets (the DB still scans skipped rows).
Cursor-based — stable, performant, slightly more complex:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTAwfQ==",
"has_more": true
}
}
GET /users?cursor=eyJpZCI6MTAwfQ==&limit=25
Use cursor-based for production APIs. Fall back to offset only when random page access is genuinely needed (admin UIs with page numbers).
Filtering and Sorting#
Keep it predictable:
GET /orders?status=shipped&created_after=2026-01-01&sort=-created_at&limit=50
Conventions:
- Filter by field name as query params:
?status=active®ion=eu - Sort with a
sortparam, prefix-for descending:?sort=-created_at,name - Date filters use ISO 8601:
?created_after=2026-01-01T00:00:00Z - Search via a dedicated
qparam:?q=john+doe
Avoid inventing custom query languages unless you are building a developer platform. Simple key-value filters cover 90% of use cases.
Error Responses: RFC 7807#
Stop inventing error formats. RFC 7807 (Problem Details for HTTP APIs) gives you a standard:
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account balance is $10.00, but the transfer requires $25.00.",
"instance": "/transfers/txn-abc-123"
}
Key fields:
- type — URI identifying the error class (machine-readable)
- title — short human summary
- status — HTTP status code (repeated for convenience)
- detail — specific explanation for this occurrence
- instance — URI for this specific error occurrence
You can extend with custom fields (e.g., errors[] for validation):
{
"type": "https://api.example.com/errors/validation",
"title": "Validation Failed",
"status": 400,
"errors": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be at least 18" }
]
}
Always return Content-Type: application/problem+json.
HATEOAS — Hypermedia Links#
HATEOAS means responses include links to related actions and resources:
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"_links": {
"self": { "href": "/users/42" },
"orders": { "href": "/users/42/orders" },
"update": { "href": "/users/42", "method": "PATCH" },
"deactivate": { "href": "/users/42/deactivate", "method": "POST" }
}
}
Benefits:
- Clients discover actions dynamically instead of hardcoding URLs
- API evolution becomes easier — add new links without breaking clients
- State-driven UIs can enable/disable buttons based on available links
In practice, full HATEOAS adoption is rare. At minimum, include self links and pagination links. Add action links where they reduce client-side URL construction.
API Documentation with OpenAPI#
OpenAPI (formerly Swagger) is the industry standard. A minimal spec:
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/orders:
get:
summary: List orders
parameters:
- name: status
in: query
schema:
type: string
enum: [pending, shipped, delivered]
responses:
'200':
description: Order list
content:
application/json:
schema:
$ref: '#/components/schemas/OrderList'
Best practices:
- Write the spec first (design-first, not code-first)
- Include request/response examples for every endpoint
- Document error responses, not just happy paths
- Generate SDKs and server stubs from the spec
- Use tools like Redocly, Stoplight, or SwaggerUI for interactive docs
Backward Compatibility#
The cardinal rule: never break existing clients.
Safe changes (backward compatible):
- Adding new optional fields to responses
- Adding new endpoints
- Adding new optional query parameters
- Adding new enum values (if clients handle unknown values)
Breaking changes (require versioning):
- Removing or renaming fields
- Changing field types
- Making optional fields required
- Changing URL structure
- Altering authentication mechanisms
Versioning Strategies#
When breaking changes are unavoidable:
URL path versioning — most common, most visible:
GET /v1/users
GET /v2/users
Header versioning — cleaner URLs, harder to test:
GET /users
Accept: application/vnd.api+json; version=2
Query parameter versioning — easy to use, feels ad hoc:
GET /users?version=2
Recommendations:
- Use URL path versioning for public APIs (discoverability wins)
- Use header versioning for internal APIs (cleaner, flexible)
- Version the API, not individual endpoints
- Support at least N-1 versions with a published deprecation timeline
- Communicate deprecation via
SunsetandDeprecationheaders (RFC 8594)
Quick Reference Checklist#
- Plural nouns, kebab-case URLs
- Correct HTTP methods with proper status codes
- Cursor-based pagination by default
- Consistent filtering and sorting conventions
- RFC 7807 error responses
- Self links at minimum, full HATEOAS where practical
- OpenAPI spec written before code
- Backward compatibility as a first-class concern
- URL path versioning for public APIs
- Deprecation timeline published and communicated
Great API design is empathy encoded in HTTP. Every decision — naming, pagination, errors — is a message to the developer consuming your work.
Build tools that developers love at codelit.io.
Article #175 in the Codelit engineering series.
Try it on Codelit
Chaos Mode
Simulate node failures and watch cascading impact across your architecture
Related articles
Try these templates
Netflix Video Streaming Architecture
Global video streaming platform with adaptive bitrate, CDN distribution, and recommendation engine.
10 componentsSearch Engine Architecture
Web-scale search with crawling, indexing, ranking, and sub-second query serving.
8 componentsMultiplayer Game Backend
Real-time multiplayer game server with matchmaking, state sync, leaderboards, and anti-cheat.
8 componentsBuild this architecture
Generate an interactive architecture for API Design Best Practices in seconds.
Try it in Codelit →
Comments