The moment a client ships code against your API, every decision you made becomes permanent. Change a field name? Their app breaks. Remove an endpoint? Their deploy fails at 2 AM. Rename a status code? Their error handling silently stops working.
Good API design isnโt about elegance โ itโs about making the right thing obvious and the wrong thing hard. This guide covers everything: resource naming, HTTP methods, status codes, error formats, versioning, pagination, authentication, idempotency, rate limiting, and documentation.
If youโre new to REST, start with What is a REST API? first. If youโre evaluating whether REST is even the right choice, compare it with GraphQL and gRPC.
Resource-oriented URLs
URLs identify resources. Theyโre nouns, not verbs.
โ
Good
GET /users
GET /users/42
GET /users/42/orders
POST /users/42/orders
โ Bad
GET /getUsers
POST /createUser
GET /getUserOrders?userId=42
POST /deleteUser
Rules that save you from future regret:
- Plural nouns for collections:
/users, not/user - Nested resources for relationships:
/users/42/orders(orders belonging to user 42) - No verbs in URLs โ the HTTP method is the verb
- Lowercase, hyphen-separated:
/order-items, not/orderItemsor/OrderItems - No trailing slashes:
/users, not/users/ - Keep nesting shallow โ two levels max. Instead of
/users/42/orders/7/items/3, use/order-items/3
For actions that donโt map cleanly to CRUD, use a sub-resource:
POST /users/42/activate โ better than POST /activateUser
POST /orders/7/cancel โ better than POST /cancelOrder
POST /reports/generate โ for one-off operations
HTTP methods โ use them correctly
Each method has a specific meaning. Donโt use POST for everything.
GET /users โ List users (safe, cacheable)
GET /users/42 โ Get one user (safe, cacheable)
POST /users โ Create a user (not idempotent)
PUT /users/42 โ Replace user 42 entirely (idempotent)
PATCH /users/42 โ Update specific fields (idempotent)
DELETE /users/42 โ Delete user 42 (idempotent)
Key distinctions people get wrong:
- PUT replaces the entire resource. If you send a PUT without the
emailfield, the email gets removed. PATCH only updates the fields you send. - GET must never change state. No
GET /users/42/delete. Crawlers will follow that link and delete your data. - POST is the only non-idempotent method. Sending the same POST twice creates two resources. This matters โ see idempotency.
- DELETE should be idempotent. Deleting something thatโs already deleted returns
204, not404.
Status codes
Donโt return 200 OK for everything. Status codes tell clients what happened without parsing the body.
| Code | Name | When to use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH |
| 201 | Created | Successful POST โ include Location header |
| 204 | No Content | Successful DELETE โ no response body |
| 301 | Moved Permanently | Resource URL changed permanently |
| 304 | Not Modified | Cached response is still valid |
| 400 | Bad Request | Malformed JSON, missing required fields |
| 401 | Unauthorized | No credentials or invalid token |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate entry, version conflict |
| 422 | Unprocessable Entity | Valid JSON but failed validation |
| 429 | Too Many Requests | Rate limit exceeded โ include Retry-After header |
| 500 | Internal Server Error | Unhandled exception โ never expose stack traces |
| 502 | Bad Gateway | Upstream service failed |
| 503 | Service Unavailable | Maintenance or overloaded โ include Retry-After |
The difference between 400 and 422 trips people up. Use 400 when the request is syntactically broken (malformed JSON). Use 422 when the JSON is valid but the data fails business rules (email already taken, amount is negative).
For the full list with examples, see the HTTP Status Codes cheat sheet.
Consistent error format
Every error response should use the same JSON envelope. Clients shouldnโt have to guess the shape.
{
"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"
}
],
"requestId": "req_abc123"
}
}
Rules for error responses:
- Machine-readable
codeโ clients switch on this, not the message string - Human-readable
messageโ for developers reading logs - Field-level
detailsfor validation errors โ so forms can highlight the right fields requestIdin every response โ makes debugging support tickets possible- Never expose internal details โ no stack traces, no SQL queries, no file paths in production
For a deep dive on error handling patterns, error codes, and retry logic, see How to Handle API Errors.
Versioning
APIs change. Versioning lets you evolve without breaking existing clients.
The three main approaches:
URL path: GET /v1/users โ most common, most visible
Header: Accept: application/vnd.api+json;version=2
Query param: GET /users?version=1 โ least common
URL path versioning wins in practice because itโs visible, cacheable, and easy to route. But there are real tradeoffs โ header versioning keeps URLs clean, and some teams use content negotiation for fine-grained control.
The full breakdown with migration strategies: REST API Versioning Strategies.
Pagination
Never return unbounded lists. A GET /users that returns 2 million rows will kill your server and the client.
Two common patterns:
Offset-based (simple, but skips items when data changes):
GET /users?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 1847
}
}
Cursor-based (stable, better for real-time data):
GET /users?cursor=eyJpZCI6NDJ9&limit=20
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6NjJ9",
"hasMore": true
}
}
Cursor-based pagination is more reliable for large datasets because inserting or deleting rows doesnโt cause items to be skipped or duplicated. Offset-based is simpler to implement and lets users jump to arbitrary pages.
For keyset pagination, infinite scroll patterns, and performance comparisons: Pagination Patterns Explained.
Authentication
Your API needs to know whoโs calling. The main options:
- API keys โ simple, good for server-to-server. Send in a header, never in the URL.
- OAuth 2.0 / OIDC โ the standard for user-facing apps. Supports scopes, refresh tokens, third-party access.
- JWT bearer tokens โ stateless, but you canโt revoke them without extra infrastructure.
- Session cookies โ fine for same-origin web apps, but donโt work for mobile or third-party clients.
Always use HTTPS. Always. An API key sent over HTTP is a password on a postcard.
For a detailed comparison with implementation examples: API Authentication Compared.
Idempotency
Network failures happen. Clients retry. If a POST /payments gets retried, does the customer get charged twice?
Idempotent operations produce the same result regardless of how many times theyโre called. GET, PUT, and DELETE are idempotent by definition. POST is not โ which is why payment APIs use idempotency keys:
POST /payments
Idempotency-Key: pay_req_abc123
Content-Type: application/json
{
"amount": 5000,
"currency": "usd",
"customer": "cus_42"
}
The server stores the result keyed by pay_req_abc123. If the same key comes in again, it returns the stored result instead of processing a new payment.
This is critical for any operation involving money, inventory, or state changes that canโt be undone. Full implementation guide: Idempotency in APIs.
Rate limiting
Without rate limiting, one misbehaving client can take down your entire API. At minimum:
- Set per-client limits (e.g., 1000 requests/minute)
- Return
429 Too Many Requestswith aRetry-Afterheader - Include rate limit headers so clients can self-throttle:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 247
X-RateLimit-Reset: 1716134400
The algorithms behind this (token bucket, sliding window, leaky bucket) matter more than youโd think. A fixed-window counter has a burst problem at window boundaries that can double your effective limit. See How Rate Limiting Actually Works for the full breakdown.
Filtering, sorting, and field selection
Let clients ask for exactly what they need:
GET /users?status=active&role=admin โ filtering
GET /users?sort=-created_at,name โ sorting (- prefix = descending)
GET /users?fields=id,name,email โ sparse fieldsets
Conventions that work:
- Use query parameters for filtering โ donโt create separate endpoints for each filter
- Prefix with
-for descending sort - Support comma-separated values for multi-select:
?status=active,pending - Document which fields are filterable โ not every field should be
HATEOAS
HATEOAS (Hypermedia as the Engine of Application State) means including links in responses so clients can discover available actions:
{
"id": 42,
"name": "Alice",
"status": "active",
"_links": {
"self": { "href": "/users/42" },
"orders": { "href": "/users/42/orders" },
"deactivate": { "href": "/users/42/deactivate", "method": "POST" }
}
}
In theory, this makes APIs self-documenting and evolvable. In practice, most APIs skip it. Clients hardcode URLs anyway, and the added payload size isnโt worth it for most use cases. GitHubโs API is a notable exception that does it well.
If youโre building a public API with many consumers, consider it. For internal APIs, good documentation is usually enough.
Webhooks
APIs arenโt always request-response. When something happens asynchronously โ a payment completes, a build finishes, a user signs up โ webhooks push the event to the client instead of making them poll.
POST https://your-app.com/webhooks/payments
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
{
"event": "payment.completed",
"data": {
"id": "pay_42",
"amount": 5000,
"currency": "usd"
},
"timestamp": "2026-05-19T14:30:00Z"
}
Always sign webhooks so receivers can verify they came from you. Always implement retry with exponential backoff for failed deliveries. For architecture patterns, security, and delivery guarantees: Webhook Architecture Patterns.
Documentation
An undocumented API doesnโt exist. Developers wonโt use what they canโt understand.
What good API docs include:
- Authentication โ how to get and use credentials
- Every endpoint โ method, URL, parameters, request/response examples
- Error codes โ what each error code means and how to fix it
- Rate limits โ what the limits are and how to handle 429s
- Changelog โ what changed and when
OpenAPI (Swagger) specs are the standard. They generate interactive docs, client SDKs, and can validate requests at runtime.
The difference between docs developers actually read and docs they ignore: API Documentation Developers Read.
API gateways
As your API grows, youโll need a layer in front of your services that handles cross-cutting concerns: authentication, rate limiting, request routing, logging, and SSL termination.
Thatโs an API gateway. You can use managed solutions (AWS API Gateway, Kong, Apigee) or build your own for full control. If you want to understand the internals: Build an API Gateway From Scratch.
Design checklist
Before shipping an API endpoint, run through this:
- URLs use plural nouns, no verbs
- HTTP methods match the operation (GET reads, POST creates, etc.)
- Status codes are specific (not just 200 and 500)
- Error responses use a consistent JSON envelope with machine-readable codes
- Pagination is implemented for all list endpoints
- Authentication is required and uses HTTPS
- Rate limiting is in place with proper headers
- Idempotency keys are supported for non-idempotent operations
- API is versioned
- Every endpoint is documented with examples
Where to go from here
This guide covers the principles. The cluster articles go deep on each topic:
- REST API Versioning Strategies โ URL vs. header vs. query param, migration playbooks
- API Authentication Compared โ API keys, OAuth, JWT, sessions side by side
- How to Handle API Errors โ error envelopes, retry logic, circuit breakers
- Pagination Patterns Explained โ offset, cursor, keyset with performance benchmarks
- Idempotency in APIs โ implementation patterns for payments and state changes
- API Documentation Developers Read โ OpenAPI, examples, and what makes docs actually useful
- Webhook Architecture Patterns โ delivery guarantees, signatures, retry strategies
- Build an API Gateway From Scratch โ routing, middleware, and rate limiting internals
And the foundational references:
- What is a REST API? โ the basics if youโre just getting started
- What is GraphQL? โ when REST isnโt the right fit
- What is gRPC? โ for high-performance service-to-service communication
- How Rate Limiting Actually Works โ token bucket, sliding window, and leaky bucket algorithms
- HTTP Status Codes Cheat Sheet โ the complete reference