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
AI Agent Tool Use Architecture: Function Calling, ReAct Loops & Structured Outputs
6 min read
AI searchAI-Powered Search Architecture: Semantic Search, Hybrid Search, and RAG
8 min read
AI safetyAI Safety Guardrails Architecture: Input Validation, Output Filtering, and Human-in-the-Loop
8 min read
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