Quick Reference
HTTP methods, essential status codes, and common headers at a glance. The cheat sheet you keep on the desk.
HTTP Methods at a Glance
| Method | Purpose | Idempotent | Safe | Request Body |
|---|---|---|---|---|
| GET | Retrieve a resource or collection | Yes | Yes | No |
| POST | Create a new resource | No | No | Yes |
| PUT | Replace a resource entirely | Yes | No | Yes |
| PATCH | Partially update a resource | No* | No | Yes |
| DELETE | Remove a resource | Yes | No | Optional |
Idempotent means calling the same request multiple times produces the same result. Safe means the method does not modify server state. *PATCH can be idempotent depending on implementation.
Essential Status Codes
| Code | Name | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, or DELETE |
| 201 | Created | Successful POST that creates a resource |
| 204 | No Content | Successful DELETE with no response body |
| 301 | Moved Permanently | Resource URL has permanently changed |
| 304 | Not Modified | Cached version is still valid |
| 400 | Bad Request | Malformed syntax or invalid parameters |
| 401 | Unauthorized | Missing or invalid authentication |
| 403 | Forbidden | Authenticated but lacks permission |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | Request conflicts with current state |
| 422 | Unprocessable Entity | Valid syntax but semantic errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unhandled server-side failure |
| 503 | Service Unavailable | Server temporarily overloaded or in maintenance |
Common Headers
Content-Type
MIME type of the request body
Content-Type: application/json
Accept
Expected response format
Accept: application/json
Authorization
Authentication credentials
Authorization: Bearer <token>
Location
URL of newly created resource
Location: /api/users/42
Cache-Control
Caching directives
Cache-Control: max-age=3600
X-Request-ID
Correlation ID for tracing
X-Request-ID: a1b2c3d4-e5f6
REST Fundamentals
The architectural constraints, core concepts, and maturity model that define RESTful design.
The 6 REST Constraints
1. Client-Server
Client and server are decoupled. The client handles presentation, the server manages data storage. Each can evolve independently.
2. Stateless
Every request contains all the information needed to process it. The server does not store session state between requests.
3. Cacheable
Responses must define themselves as cacheable or non-cacheable. Proper caching eliminates redundant interactions and improves scalability.
4. Uniform Interface
A consistent, predictable interface: resource identification through URIs, manipulation through representations, self-descriptive messages, and HATEOAS.
5. Layered System
The architecture is composed of layers (proxies, gateways, load balancers). Each component only knows about the layer it directly interacts with.
6. Code on Demand (Optional)
Servers can extend client functionality by sending executable code (JavaScript, applets). This is the only optional REST constraint.
Resources vs Representations
Resource
An abstract concept or entity identified by a URI. A resource is the thing itself — a user, an order, a product.
# The resource (conceptual entity)
/api/users/42
# This URI always refers to user 42
# regardless of format or state
Representation
The concrete data format returned when a client requests a resource. The same resource can have multiple representations.
# JSON representation
GET /api/users/42
Accept: application/json
{
"id": 42,
"name": "Ada Lovelace",
"email": "ada@example.com"
}
# XML representation (same resource)
GET /api/users/42
Accept: application/xml
<user>
<id>42</id>
<name>Ada Lovelace</name>
</user>
Richardson Maturity Model
A model that grades APIs by how well they use HTTP and REST principles. Each level builds on the one below.
| Level | Name | Description | Example |
|---|---|---|---|
| 0 | The Swamp of POX | Single URI, single HTTP method (usually POST). HTTP as transport only. | POST /api with action in body |
| 1 | Resources | Multiple URIs, but still only one HTTP method. Individual resources identified. | POST /api/users, POST /api/orders |
| 2 | HTTP Verbs | Multiple URIs and proper HTTP methods. The standard most APIs target. | GET /users, POST /users, DELETE /users/42 |
| 3 | Hypermedia (HATEOAS) | Responses include links to related actions. The API is self-describing and discoverable. | Response includes "_links": { "self": ..., "orders": ... } |
Most production APIs target Level 2. Level 3 (HATEOAS) is aspirational for many teams but provides significant benefits for API discoverability and evolvability.
REST vs RPC vs GraphQL
| Aspect | REST | RPC (gRPC) | GraphQL |
|---|---|---|---|
| Paradigm | Resource-oriented | Action-oriented | Query-oriented |
| Transport | HTTP/1.1, HTTP/2 | HTTP/2 (Protocol Buffers) | HTTP (typically POST) |
| Data format | JSON, XML, etc. | Protobuf (binary) | JSON |
| Endpoint style | GET /users/42 |
GetUser(id: 42) |
{ user(id: 42) { name } } |
| Overfetching | Common (fixed schemas) | Minimal (typed contracts) | Eliminated (client picks fields) |
| Caching | HTTP caching built in | Custom caching needed | Complex (single endpoint) |
| Best for | Public APIs, CRUD | Microservices, streaming | Complex UIs, mobile apps |
# REST approach
GET /api/users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
# RPC approach (gRPC-style)
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
# GraphQL approach
POST /graphql
{
"query": "{ user(id: 42) { name email orders { id total } } }"
}
URL Design
Resource naming, path structure, and query parameter conventions. The address system of your API.
Resource Naming Rules
Use Nouns, Not Verbs
Resources are things, not actions. The HTTP method provides the verb.
# Good
GET /users
POST /orders
# Bad
GET /getUsers
POST /createOrder
Plural Collections
Use plural nouns for collections. Singular for a specific resource within.
# Collection
GET /api/articles
# Single resource
GET /api/articles/507
Kebab-Case
Use hyphens for multi-word resource names. Lowercase only.
# Good
/api/user-profiles
/api/order-items
# Bad
/api/userProfiles
/api/order_items
No Trailing Slashes
Trailing slashes add no semantic value and create duplicate URIs.
# Good
/api/users
# Bad
/api/users/
No File Extensions
Use the Accept header for content negotiation, not the URL.
# Good
GET /api/reports/annual
Accept: application/pdf
# Bad
GET /api/reports/annual.pdf
Consistent Hierarchy
Paths reflect the relationship between resources.
# Hierarchy reflects ownership
/api/users/42/orders
/api/users/42/orders/7
/api/shops/12/products
Hierarchical Path Structure
URL paths read like an address, narrowing from general to specific.
# Pattern: /{version}/{resource}/{id}/{sub-resource}/{sub-id}
# API root
/api/v1
# Collection of users
/api/v1/users
# A specific user
/api/v1/users/42
# Orders belonging to user 42
/api/v1/users/42/orders
# A specific order belonging to user 42
/api/v1/users/42/orders/107
# Line items within that order
/api/v1/users/42/orders/107/items
Path Anatomy
Nested Resources: When to Nest vs Flatten
Nest When...
The child resource only exists within the context of the parent. It has no meaning on its own.
# Order items belong to an order
GET /orders/107/items
GET /orders/107/items/3
# Comments belong to a post
GET /posts/88/comments
# Seats belong to an event
GET /events/5/seats
Flatten When...
The child can exist independently, or you need to query across parents.
# Products exist independently of shops
GET /products?shop_id=12
GET /products/99
# Tags can belong to many resources
GET /tags?resource=posts
# Users exist outside of teams
GET /users?team_id=4
Limit nesting to one level deep. /users/42/orders is fine. /users/42/orders/107/items/3/reviews is too deep — flatten with query parameters instead.
Query Parameters
Query parameters handle filtering, sorting, searching, and pagination — anything that does not identify a resource.
# Filtering
GET /api/products?category=electronics&in_stock=true
# Sorting (prefix with - for descending)
GET /api/products?sort=-price,name
# Searching
GET /api/products?q=wireless+headphones
# Pagination
GET /api/products?page=2&per_page=25
# Field selection (sparse fieldsets)
GET /api/products?fields=id,name,price
# Combining everything
GET /api/products?category=electronics&sort=-price&page=1&per_page=10&fields=id,name,price
| Parameter | Purpose | Example |
|---|---|---|
sort |
Order results by field(s) | ?sort=-created_at,name |
fields |
Select specific fields to return | ?fields=id,title,author |
q |
Full-text search query | ?q=machine+learning |
page / per_page |
Offset-based pagination | ?page=3&per_page=20 |
cursor |
Cursor-based pagination | ?cursor=eyJpZCI6MTAwfQ== |
expand |
Include related resources inline | ?expand=author,comments |
URL Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI Path | /api/v1/users |
Simple, visible, easy to route | Pollutes the URI space |
| Query Param | /api/users?v=1 |
Optional, easy to default | Easy to forget, caching issues |
| Custom Header | X-API-Version: 1 |
Clean URIs | Hidden, harder to test |
| Accept Header | Accept: application/vnd.api.v1+json |
RESTful, content negotiation | Complex, harder to explore |
| Subdomain | v1.api.example.com |
Clean separation | DNS/infrastructure overhead |
URI path versioning (/api/v1/) is the most common and pragmatic choice. It is visible, easy to document, and simple to route. Use it unless you have a specific reason to prefer another strategy.
HTTP Methods
The verbs of the web. Each method carries specific semantics about what the client intends the server to do.
Method Overview
| Method | Purpose | Idempotent | Safe | Cacheable | Request Body | Response Body |
|---|---|---|---|---|---|---|
| GET | Retrieve resource(s) | Yes | Yes | Yes | No | Yes |
| POST | Create a resource / trigger action | No | No | Conditional | Yes | Yes |
| PUT | Replace a resource entirely | Yes | No | No | Yes | Optional |
| PATCH | Partial update | No* | No | No | Yes | Yes |
| DELETE | Remove a resource | Yes | No | No | Optional | Optional |
| HEAD | GET without response body | Yes | Yes | Yes | No | No |
| OPTIONS | Describe communication options | Yes | Yes | No | No | Yes |
*PATCH can be idempotent when using JSON Merge Patch (RFC 7396), but is not guaranteed to be idempotent when using JSON Patch (RFC 6902) — an add operation to an array, for example, appends on every call.
GET — Retrieve Resources
Method Properties
# Retrieve a collection
GET /api/v1/articles?page=1&per_page=3 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
X-Total-Count: 142
{
"data": [
{
"id": 301,
"title": "Understanding REST Constraints",
"slug": "understanding-rest-constraints",
"author": { "id": 42, "name": "Ada Lovelace" },
"publishedAt": "2025-03-15T09:30:00Z",
"tags": ["api", "architecture"]
},
{
"id": 302,
"title": "HTTP Caching Deep Dive",
"slug": "http-caching-deep-dive",
"author": { "id": 17, "name": "Grace Hopper" },
"publishedAt": "2025-03-12T14:00:00Z",
"tags": ["http", "performance"]
},
{
"id": 303,
"title": "JSON Schema Validation",
"slug": "json-schema-validation",
"author": { "id": 42, "name": "Ada Lovelace" },
"publishedAt": "2025-03-10T11:15:00Z",
"tags": ["json", "validation"]
}
],
"pagination": {
"page": 1,
"perPage": 3,
"totalPages": 48,
"totalItems": 142
}
}
# Retrieve a single resource
GET /api/v1/articles/301 HTTP/1.1
Host: api.example.com
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "a1b2c3d4"
{
"id": 301,
"title": "Understanding REST Constraints",
"slug": "understanding-rest-constraints",
"body": "REST (Representational State Transfer) defines six constraints...",
"author": {
"id": 42,
"name": "Ada Lovelace",
"avatarUrl": "https://cdn.example.com/avatars/42.jpg"
},
"publishedAt": "2025-03-15T09:30:00Z",
"updatedAt": "2025-03-16T08:00:00Z",
"tags": ["api", "architecture"],
"readTimeMinutes": 8
}
POST — Create Resources
Method Properties
# Create a new article
POST /api/v1/articles HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"title": "Rate Limiting Strategies",
"body": "Rate limiting protects your API from abuse and ensures fair usage...",
"tags": ["api", "security", "infrastructure"],
"status": "draft"
}
# Response — 201 with Location header pointing to the new resource
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/articles/304
{
"id": 304,
"title": "Rate Limiting Strategies",
"body": "Rate limiting protects your API from abuse and ensures fair usage...",
"author": { "id": 42, "name": "Ada Lovelace" },
"tags": ["api", "security", "infrastructure"],
"status": "draft",
"createdAt": "2025-03-17T10:45:00Z",
"updatedAt": "2025-03-17T10:45:00Z"
}
Do not use POST for operations that are idempotent. If the client can safely retry the request (e.g., setting a user's email), use PUT instead. Reserve POST for non-idempotent creation or actions that trigger side effects.
PUT — Replace Resources
Method Properties
# Replace an article entirely — ALL fields required
PUT /api/v1/articles/304 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"title": "Rate Limiting Strategies for Production APIs",
"body": "Rate limiting protects your API from abuse and ensures fair usage across consumers...",
"tags": ["api", "security", "infrastructure", "production"],
"status": "published"
}
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 304,
"title": "Rate Limiting Strategies for Production APIs",
"body": "Rate limiting protects your API from abuse and ensures fair usage across consumers...",
"author": { "id": 42, "name": "Ada Lovelace" },
"tags": ["api", "security", "infrastructure", "production"],
"status": "published",
"createdAt": "2025-03-17T10:45:00Z",
"updatedAt": "2025-03-17T11:20:00Z"
}
PUT requires the complete resource representation. Any field omitted from the request body will be reset to its default or removed. If you only need to update one field, use PATCH instead.
PATCH — Partial Updates
Method Properties
JSON Merge Patch (RFC 7396)
Send only the fields you want to change. Null values remove fields. Simple and intuitive.
# Update only the title and status (other fields unchanged)
PATCH /api/v1/articles/304 HTTP/1.1
Host: api.example.com
Content-Type: application/merge-patch+json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"title": "Rate Limiting Strategies (Updated)",
"status": "published"
}
# Response — full updated resource
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 304,
"title": "Rate Limiting Strategies (Updated)",
"body": "Rate limiting protects your API from abuse...",
"tags": ["api", "security", "infrastructure", "production"],
"status": "published",
"updatedAt": "2025-03-17T12:00:00Z"
}
JSON Patch (RFC 6902)
An ordered sequence of operations. More powerful than merge patch — supports add, remove, replace, move, copy, and test.
# Apply a sequence of operations
PATCH /api/v1/articles/304 HTTP/1.1
Host: api.example.com
Content-Type: application/json-patch+json
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
[
{ "op": "replace", "path": "/title", "value": "Advanced Rate Limiting" },
{ "op": "add", "path": "/tags/-", "value": "advanced" },
{ "op": "remove", "path": "/tags/0" },
{ "op": "test", "path": "/status", "value": "published" }
]
# Response
HTTP/1.1 200 OK
Content-Type: application/json
| Aspect | JSON Merge Patch (RFC 7396) | JSON Patch (RFC 6902) |
|---|---|---|
| Content-Type | application/merge-patch+json |
application/json-patch+json |
| Format | Partial JSON object | Array of operation objects |
| Null handling | Null = remove field | Explicit "remove" operation |
| Array support | Replaces entire array | Can target individual elements |
| Idempotent | Yes | Not always (e.g., add to array) |
| Complexity | Simple, intuitive | Powerful, verbose |
| Best for | Most CRUD APIs | Complex document editing |
Use JSON Merge Patch for most APIs — it covers 90% of use cases with minimal complexity. Reserve JSON Patch for scenarios that require atomic array manipulation or conditional updates via the test operation.
DELETE — Remove Resources
Method Properties
# Delete an article
DELETE /api/v1/articles/304 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
# Response — first call
HTTP/1.1 204 No Content
# Response — second call (resource already deleted)
# Option A: still 204 (treats "already deleted" as success)
HTTP/1.1 204 No Content
# Option B: 404 (resource no longer exists)
HTTP/1.1 404 Not Found
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "Article 304 does not exist"
}
}
Both behaviors are valid for repeated DELETE calls. Returning 204 on subsequent calls is simpler for clients. Returning 404 is more informative. Pick one pattern and be consistent across your API.
HEAD & OPTIONS
HEAD — Metadata Only
Identical to GET but returns only headers, no body. Useful for checking if a resource exists or reading metadata without transferring the payload.
# Check if resource exists + get metadata
HEAD /api/v1/articles/301 HTTP/1.1
Host: api.example.com
# Response — headers only, no body
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 1842
ETag: "a1b2c3d4"
Last-Modified: Sat, 16 Mar 2025 08:00:00 GMT
OPTIONS — Communication Options
Describes allowed methods for a resource. Most commonly seen as a CORS preflight request sent automatically by browsers.
# CORS preflight request (browser-initiated)
OPTIONS /api/v1/articles HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type
# Response — server declares what is allowed
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Always handle OPTIONS requests on your API endpoints. If you do not, CORS preflight requests from browsers will fail and your API will be unusable from web frontends on different origins.
Status Codes
The three-digit signals that tell clients exactly what happened. Choose precisely — vague codes create vague APIs.
2xx — Success
The request was received, understood, and accepted.
| Code | Name | When to Use | Typical Methods |
|---|---|---|---|
| 200 | OK | General success with a response body | GET PUT PATCH |
| 201 | Created | New resource created; include Location header |
POST |
| 202 | Accepted | Request queued for async processing | POST |
| 204 | No Content | Success but no response body to return | DELETE PUT |
# 200 OK — successful retrieval
GET /api/v1/users/42
HTTP/1.1 200 OK
{ "id": 42, "name": "Ada Lovelace", "email": "ada@example.com" }
# 201 Created — new resource, with Location header
POST /api/v1/users
HTTP/1.1 201 Created
Location: /api/v1/users/43
{ "id": 43, "name": "Grace Hopper", "email": "grace@example.com" }
# 202 Accepted — queued for background processing
POST /api/v1/reports/generate
HTTP/1.1 202 Accepted
{ "jobId": "rpt-9a8b7c", "statusUrl": "/api/v1/jobs/rpt-9a8b7c", "estimatedSeconds": 30 }
# 204 No Content — successful delete, nothing to return
DELETE /api/v1/users/44
HTTP/1.1 204 No Content
3xx — Redirection
The client must take additional action to complete the request.
| Code | Name | Permanent? | Preserves Method? | Use Case |
|---|---|---|---|---|
| 301 | Moved Permanently | Yes | No (may change to GET) | Resource URL permanently changed |
| 302 | Found | No | No (may change to GET) | Temporary redirect (legacy, ambiguous) |
| 304 | Not Modified | N/A | N/A | Cached version is still valid (ETag / If-Modified-Since) |
| 307 | Temporary Redirect | No | Yes | Temporary redirect, same method |
| 308 | Permanent Redirect | Yes | Yes | Permanent redirect, same method |
Use 308 instead of 301 when you need to guarantee the HTTP method is preserved. A 301 redirect may cause clients to switch a POST to a GET, which can silently break writes. Similarly, prefer 307 over 302 for temporary redirects.
4xx — Client Errors
The request contains bad syntax or cannot be fulfilled. The fault lies with the client.
| Code | Name | When to Use |
|---|---|---|
| 400 | Bad Request | Malformed JSON, missing required fields, invalid data types |
| 401 | Unauthorized | No credentials provided or credentials are invalid/expired |
| 403 | Forbidden | Authenticated but lacks permission for this action |
| 404 | Not Found | Resource does not exist (or you want to hide its existence) |
| 405 | Method Not Allowed | Valid endpoint, wrong HTTP method (e.g., DELETE on read-only resource) |
| 409 | Conflict | Request conflicts with current state (duplicate entry, version mismatch) |
| 410 | Gone | Resource existed but has been permanently deleted (stronger than 404) |
| 415 | Unsupported Media Type | Server does not support the Content-Type sent by the client |
| 422 | Unprocessable Entity | Valid JSON but fails business logic validation (e.g., email already taken) |
| 429 | Too Many Requests | Rate limit exceeded; include Retry-After header |
400 Bad Request — the request is syntactically malformed. The server cannot parse it at all. Examples: invalid JSON, wrong data type for a field, missing required field.
422 Unprocessable Entity — the request is well-formed JSON but fails business rules. The server parsed it, but the values are semantically invalid. Examples: email already registered, end date before start date, negative quantity.
# 400 — malformed request (invalid JSON syntax)
POST /api/v1/users
Content-Type: application/json
{ "name": "Ada", "email": } <-- syntax error
HTTP/1.1 400 Bad Request
{ "error": { "code": "INVALID_JSON", "message": "Unexpected token at position 32" } }
# 422 — valid JSON but violates business rules
POST /api/v1/users
Content-Type: application/json
{ "name": "Ada", "email": "ada@example.com" }
HTTP/1.1 422 Unprocessable Entity
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Validation failed",
"details": [
{ "field": "email", "message": "Email address is already registered" }
]
}
}
401 Unauthorized — identity is unknown. The client sent no token, an expired token, or an invalid token. The fix is to authenticate (log in).
403 Forbidden — identity is known, but lacks permission. The client is authenticated but not authorized for this action. Logging in again will not help.
# 401 — no authentication provided
GET /api/v1/admin/users
(no Authorization header)
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api"
{ "error": { "code": "AUTH_REQUIRED", "message": "Authentication required" } }
# 403 — authenticated but not an admin
DELETE /api/v1/admin/users/42
Authorization: Bearer eyJ... (valid token, role=viewer)
HTTP/1.1 403 Forbidden
{ "error": { "code": "INSUFFICIENT_PERMISSIONS", "message": "Admin role required" } }
# 409 — conflict with current state
PUT /api/v1/articles/301
If-Match: "a1b2c3d4" (stale ETag)
HTTP/1.1 409 Conflict
{
"error": {
"code": "VERSION_CONFLICT",
"message": "Resource has been modified since your last read",
"currentETag": "x9y8z7w6"
}
}
# 429 — rate limit exceeded
GET /api/v1/search?q=test
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710680400
{ "error": { "code": "RATE_LIMITED", "message": "Rate limit exceeded. Try again in 30 seconds." } }
5xx — Server Errors
The server failed to fulfill a valid request. The fault lies with the server.
| Code | Name | Meaning | Quick Description |
|---|---|---|---|
| 500 | Internal Server Error | Unhandled exception or bug | Something broke on our end |
| 502 | Bad Gateway | Upstream returned invalid response | Proxy got a bad response from the origin |
| 503 | Service Unavailable | Intentionally down (maintenance, overload) | We are temporarily offline |
| 504 | Gateway Timeout | Upstream did not respond in time | Proxy timed out waiting for the origin |
500 = unhandled bug in your code. 502 = your proxy received a garbled response from the upstream server. 503 = your service is intentionally unavailable (deploy, maintenance, overload). 504 = your proxy waited too long and the upstream never responded. For 503, always include a Retry-After header.
Request & Response Design
The anatomy of HTTP messages. Headers, bodies, content negotiation, and the conventions that keep JSON APIs consistent.
Request Anatomy
Every HTTP request consists of four parts: method + path, headers, an optional body, and query parameters encoded in the URL.
# [1] Request line: METHOD PATH PROTOCOL
POST /api/v1/articles HTTP/1.1
# [2] Headers: metadata about the request
Host: api.example.com
Content-Type: application/json ← format of the body we are sending
Accept: application/json ← format we want back
Authorization: Bearer eyJhbGci... ← authentication credential
X-Request-ID: req-7f3a9b2c ← correlation ID for tracing
Content-Length: 147 ← size of the body in bytes
# [3] Blank line separates headers from body
# [4] Body: the payload (JSON, form data, etc.)
{
"title": "Request Anatomy Explained",
"body": "Every HTTP request has four parts...",
"tags": ["http", "tutorial"],
"status": "draft"
}
Response Anatomy
# [1] Status line: PROTOCOL STATUS-CODE REASON
HTTP/1.1 201 Created
# [2] Response headers
Content-Type: application/json ← format of the response body
Location: /api/v1/articles/305 ← URL of the newly created resource
ETag: "e8f9a0b1" ← version tag for conditional requests
Cache-Control: no-cache ← caching directive
X-Request-ID: req-7f3a9b2c ← echoed back for correlation
X-RateLimit-Limit: 1000 ← rate limit ceiling
X-RateLimit-Remaining: 997 ← remaining requests in window
# [3] Blank line
# [4] Response body
{
"id": 305,
"title": "Request Anatomy Explained",
"body": "Every HTTP request has four parts...",
"author": { "id": 42, "name": "Ada Lovelace" },
"tags": ["http", "tutorial"],
"status": "draft",
"createdAt": "2025-03-18T09:00:00Z",
"updatedAt": "2025-03-18T09:00:00Z"
}
Essential Headers
| Header | Direction | Purpose | Example Value |
|---|---|---|---|
Content-Type |
Both | MIME type of the message body | application/json |
Accept |
Request | Preferred response format(s) | application/json, text/html;q=0.9 |
Authorization |
Request | Authentication credentials | Bearer eyJhbGciOi... |
Cache-Control |
Both | Caching directives | max-age=3600, public |
ETag |
Response | Resource version identifier | "a1b2c3d4" |
If-None-Match |
Request | Conditional GET (cache validation) | "a1b2c3d4" |
If-Match |
Request | Conditional PUT/PATCH (optimistic locking) | "a1b2c3d4" |
Location |
Response | URL of created/redirected resource | /api/v1/users/43 |
Retry-After |
Response | Seconds to wait before retrying (429, 503) | 30 |
X-Request-ID |
Both | Correlation ID for distributed tracing | req-7f3a9b2c |
Content Negotiation
Clients use the Accept header to tell the server which response formats they prefer. Quality values (q) express relative preference from 0 to 1.
# Client prefers JSON, but will accept XML as a fallback
GET /api/v1/articles/301 HTTP/1.1
Accept: application/json, application/xml;q=0.8, text/plain;q=0.5
# Server responds with the best match
HTTP/1.1 200 OK
Content-Type: application/json
Vary: Accept
{ "id": 301, "title": "Understanding REST Constraints" }
# If the server cannot satisfy any requested format:
HTTP/1.1 406 Not Acceptable
{
"error": {
"code": "NOT_ACCEPTABLE",
"message": "Supported formats: application/json, application/xml",
"supportedMediaTypes": ["application/json", "application/xml"]
}
}
Always include a Vary: Accept header when your response format depends on content negotiation. This ensures caches store separate entries for each format and do not serve JSON to a client expecting XML.
JSON Conventions
camelCase Properties
Use camelCase for all JSON property names. It matches JavaScript conventions and is the most common API standard.
# Good
{
"firstName": "Ada",
"createdAt": "2025-03-15T09:30:00Z",
"isActive": true
}
# Bad
{
"first_name": "Ada",
"created-at": "...",
"IsActive": true
}
ISO 8601 Dates
Always use ISO 8601 format with timezone. UTC is preferred. Never use Unix timestamps in public APIs.
# Good — ISO 8601 with UTC
{
"createdAt": "2025-03-15T09:30:00Z",
"expiresAt": "2025-04-15T00:00:00Z"
}
# Bad — ambiguous formats
{
"created": 1710495000,
"expires": "03/15/2025"
}
Null Handling
Include nullable fields with explicit null. Do not omit them — clients need to distinguish "not set" from "not returned".
# Good — explicit null
{
"id": 42,
"name": "Ada Lovelace",
"bio": null,
"avatarUrl": null
}
# Bad — missing fields
{
"id": 42,
"name": "Ada Lovelace"
}
Envelope Pattern vs Flat Responses
Envelope pattern — wrap data in a container. Useful for collections where you need pagination metadata alongside the data.
# Envelope — collection with metadata
{
"data": [
{ "id": 1, "name": "Ada" },
{ "id": 2, "name": "Grace" }
],
"pagination": {
"page": 1,
"perPage": 20,
"totalItems": 142
}
}
Flat response — return the resource directly. Simpler for single-resource endpoints. Use HTTP headers for metadata.
# Flat — single resource, no wrapper
{
"id": 42,
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "admin"
}
# Metadata goes in headers
ETag: "a1b2c3d4"
Last-Modified: Sat, 15 Mar 2025 09:30:00 GMT
Use flat responses for single resources and envelope pattern for collections. This gives you a consistent place for pagination metadata without wrapping every single response in unnecessary nesting.
Common Operations — Full HTTP Conversations
List Resources
# Request
GET /api/v1/users?role=admin&sort=-createdAt&page=1&per_page=2 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
X-Total-Count: 8
{
"data": [
{ "id": 42, "name": "Ada Lovelace", "email": "ada@example.com", "role": "admin", "createdAt": "2025-01-10T08:00:00Z" },
{ "id": 17, "name": "Grace Hopper", "email": "grace@example.com", "role": "admin", "createdAt": "2025-01-05T12:00:00Z" }
],
"pagination": { "page": 1, "perPage": 2, "totalPages": 4, "totalItems": 8 }
}
Get Single Resource
# Request
GET /api/v1/users/42 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGci...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "u42-v7"
{
"id": 42,
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "admin",
"bio": "Mathematician and writer, known for work on the Analytical Engine.",
"avatarUrl": "https://cdn.example.com/avatars/42.jpg",
"createdAt": "2025-01-10T08:00:00Z",
"updatedAt": "2025-03-15T09:30:00Z"
}
Create Resource
# Request
POST /api/v1/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGci...
{
"name": "Alan Turing",
"email": "alan@example.com",
"role": "editor"
}
# Response
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/v1/users/55
{
"id": 55,
"name": "Alan Turing",
"email": "alan@example.com",
"role": "editor",
"bio": null,
"avatarUrl": null,
"createdAt": "2025-03-18T10:15:00Z",
"updatedAt": "2025-03-18T10:15:00Z"
}
Update Resource
# Request — partial update with merge patch
PATCH /api/v1/users/55 HTTP/1.1
Host: api.example.com
Content-Type: application/merge-patch+json
Authorization: Bearer eyJhbGci...
If-Match: "u55-v1"
{
"bio": "Pioneer of theoretical computer science and artificial intelligence.",
"role": "admin"
}
# Response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "u55-v2"
{
"id": 55,
"name": "Alan Turing",
"email": "alan@example.com",
"role": "admin",
"bio": "Pioneer of theoretical computer science and artificial intelligence.",
"avatarUrl": null,
"createdAt": "2025-03-18T10:15:00Z",
"updatedAt": "2025-03-18T10:30:00Z"
}
Delete Resource
# Request
DELETE /api/v1/users/55 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
# Response
HTTP/1.1 204 No Content
Authentication & Security
Protecting your API. Keys, tokens, flows, and the guardrails that keep everything locked down.
API Keys
API keys identify the calling application. They are not user credentials — they tell the server which app is making the request, not who the user is.
Header-Based Keys (Recommended)
# Request with API key in header
GET /api/v1/products HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_a1b2c3d4e5f6g7h8i9j0
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{ "id": 1, "name": "Widget", "price": 9.99 }
]
}
Never pass API keys as query parameters (?api_key=sk_live_...). Query strings are logged in server access logs, browser history, proxy logs, and CDN caches. Header values are not. Always use X-API-Key or Authorization headers.
Key Prefixes for Safety
# Use prefixes to distinguish key types
sk_live_a1b2c3d4... # Secret key — server-side only
pk_live_x9y8z7w6... # Publishable key — safe for client-side
sk_test_m4n5o6p7... # Test key — sandbox environment
OAuth 2.0
OAuth 2.0 delegates authorization without sharing passwords. The client receives a token that grants limited access on behalf of a user (or itself).
Authorization Code + PKCE (Web & Mobile Apps)
The most secure flow for apps where a user is present. PKCE (Proof Key for Code Exchange) prevents authorization code interception attacks.
# Step 1 — Generate a code verifier and challenge
code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
code_challenge = BASE64URL(SHA256(code_verifier))
= "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
# Step 2 — Redirect user to authorization server
GET https://auth.example.com/authorize
?response_type=code
&client_id=app_abc123
&redirect_uri=https://app.example.com/callback
&scope=read:users write:posts
&state=xYz789randomStateValue
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
# Step 3 — User authenticates, server redirects back with code
https://app.example.com/callback
?code=SplxlOBeZQQYbYS6WxSbIA
&state=xYz789randomStateValue
# Step 4 — Exchange code for tokens (server-side)
POST https://auth.example.com/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA
&redirect_uri=https://app.example.com/callback
&client_id=app_abc123
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
# Step 5 — Receive tokens
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
"scope": "read:users write:posts"
}
Client Credentials (Machine-to-Machine)
For server-to-server communication with no user context. The application authenticates as itself.
# Request a token using client credentials
POST https://auth.example.com/token HTTP/1.1
Content-Type: application/x-www-form-urlencoded
Authorization: Basic BASE64(client_id:client_secret)
grant_type=client_credentials
&scope=read:analytics
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "read:analytics"
}
JWT (JSON Web Tokens)
A JWT is a compact, URL-safe token with three Base64URL-encoded parts separated by dots: header.payload.signature.
# JWT structure: header.payload.signature
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiJ1c2VyXzQyIiwibmFtZSI6IkFkYS
BMb3ZlbGFjZSIsInJvbGUiOiJhZG1pbiIsImlhd
CI6MTcxMDUwMDAwMCwiZXhwIjoxNzEwNTAzNjAw
LCJpc3MiOiJhcGkuZXhhbXBsZS5jb20iLCJhdWQ
iOiJhcHBfYWJjMTIzIn0.
SIGNATURE_BYTES_HERE
# Decoded header
{
"alg": "RS256",
"typ": "JWT"
}
# Decoded payload
{
"sub": "user_42",
"name": "Ada Lovelace",
"role": "admin",
"iat": 1710500000,
"exp": 1710503600,
"iss": "api.example.com",
"aud": "app_abc123"
}
JWT Validation Checklist
Algorithm (alg)
Verify the alg header matches your expected algorithm. Never accept "none". Use asymmetric algorithms (RS256, ES256) for public APIs.
Signature
Validate the signature using the correct key. For RS256, use the issuer's public key. Reject the token if the signature does not verify.
Expiration (exp)
Reject tokens where exp is in the past. Allow a small clock skew tolerance (30–60 seconds) for distributed systems.
Issuer (iss)
Confirm iss matches the expected authorization server. This prevents tokens issued by a different server from being accepted.
Audience (aud)
Confirm aud includes your API's identifier. This prevents a token intended for one API from being replayed against another.
Not Before (nbf)
If present, reject tokens where nbf is in the future. The token should not be used before this time.
Bearer Tokens
Bearer tokens are the standard way to pass access tokens in API requests. Any party holding the token can use it — hence the name "bearer."
# Using a bearer token
GET /api/v1/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Accept: application/json
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "user_42",
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "admin"
}
# If the token is invalid or expired:
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token",
error_description="The access token has expired"
The Authorization header format is Bearer <token> with a single space after "Bearer." The token itself is opaque to the client — it could be a JWT, a random string, or any other format. The client should not parse it.
CORS (Cross-Origin Resource Sharing)
CORS controls which origins (domains) can access your API from a browser. The browser sends a preflight OPTIONS request before the actual request to check permissions.
Preflight Request & Response
# Browser sends preflight OPTIONS request
OPTIONS /api/v1/users HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization
# Server responds with CORS headers
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization, X-API-Key
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
Common CORS Headers
| Header | Purpose | Example |
|---|---|---|
Access-Control-Allow-Origin |
Which origins can access the resource | https://app.example.com |
Access-Control-Allow-Methods |
Allowed HTTP methods | GET, POST, PUT, DELETE |
Access-Control-Allow-Headers |
Allowed request headers | Content-Type, Authorization |
Access-Control-Max-Age |
How long to cache preflight (seconds) | 86400 |
Access-Control-Allow-Credentials |
Whether cookies/auth headers are allowed | true |
Access-Control-Expose-Headers |
Response headers the browser can read | X-RateLimit-Remaining |
Never use Access-Control-Allow-Origin: * together with Access-Control-Allow-Credentials: true. Browsers will reject this combination. When credentials are needed, you must specify the exact origin.
Rate Limiting
Rate limiting protects your API from abuse and ensures fair usage. The token bucket algorithm is the most common approach: each client has a bucket of tokens that refills at a steady rate. Each request consumes one token.
Rate Limit Headers
# Successful request with rate limit headers
GET /api/v1/products HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1710504000
{
"data": [ ... ]
}
| Header | Meaning |
|---|---|
X-RateLimit-Limit |
Maximum requests allowed in the current window |
X-RateLimit-Remaining |
Requests remaining in the current window |
X-RateLimit-Reset |
Unix timestamp when the window resets |
429 Too Many Requests
# Rate limit exceeded
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710504000
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have exceeded 1000 requests per hour. Try again in 30 seconds.",
"retryAfter": 30
}
Always include a Retry-After header with 429 responses. Clients need to know when they can resume making requests. Use seconds (integer) or an HTTP date.
Security Checklist
HTTPS Everywhere
Serve all API endpoints over TLS. Redirect HTTP to HTTPS. Use HSTS headers. No exceptions, even for internal APIs.
Validate All Input
Validate type, length, format, and range on every field. Reject unexpected fields. Never trust client data regardless of source.
Parameterized Queries
Never concatenate user input into SQL or NoSQL queries. Use parameterized queries or an ORM to prevent injection attacks.
Short Token Lifetimes
Access tokens: 15–60 minutes. Refresh tokens: days to weeks with rotation. Revoke tokens on password change or suspicious activity.
Role-Based Access (RBAC)
Check permissions on every request. Verify the user can access the specific resource, not just the endpoint. Never rely on client-side checks alone.
Monitor API Usage
Log all authentication events. Track anomalous patterns (geographic, volume, timing). Set up alerts for brute force and credential stuffing attempts.
Pagination, Filtering & Sorting
Slicing through large datasets. Offset, cursor, filters, and the query parameters that shape every collection response.
Why Paginate
Returning an entire collection in one response does not scale. A table with 100,000 rows serialized to JSON can exceed 50 MB and take seconds to transmit. Pagination limits response size, reduces database load, and improves perceived performance.
Add pagination from day one. Retrofitting pagination on an existing API is a breaking change — clients already depend on receiving the full collection. Even if your table has only 20 rows today, it will grow.
Offset Pagination
The simplest approach. The client specifies how many items to skip (offset) and how many to return (limit). Maps directly to SQL's LIMIT and OFFSET clauses.
# Request — page 6 (items 101-120)
GET /api/v1/products?limit=20&offset=100 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{ "id": 101, "name": "Ergonomic Keyboard", "price": 89.00 },
{ "id": 102, "name": "USB-C Hub", "price": 45.00 },
... 18 more items ...
],
"pagination": {
"limit": 20,
"offset": 100,
"totalItems": 2340,
"totalPages": 117,
"currentPage": 6,
"hasNextPage": true,
"hasPrevPage": true
}
}
Pros
- Simple to implement and understand
- Clients can jump to any page directly
- Easy to display "Page X of Y" in the UI
- Total count enables progress indicators
Cons
- Slow at high offsets (database scans skipped rows)
- Inconsistent results when data changes between pages
COUNT(*)queries are expensive on large tables- Items can be skipped or duplicated during pagination
Cursor Pagination
Instead of counting rows, the server returns an opaque cursor pointing to the last item in the current page. The client passes this cursor to fetch the next page. The cursor is typically a Base64-encoded value of the sort key.
# Request — first page
GET /api/v1/products?limit=20 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{ "id": 1, "name": "Mechanical Keyboard", "price": 129.00 },
{ "id": 2, "name": "Trackball Mouse", "price": 79.00 },
... 18 more items ...
],
"pagination": {
"limit": 20,
"hasNextPage": true,
"hasPrevPage": false,
"nextCursor": "eyJpZCI6MjAsImNyZWF0ZWRBdCI6IjIwMjUtMDMtMTVUMDk6MzA6MDBaIn0",
"prevCursor": null
}
}
# Request — next page using cursor
GET /api/v1/products?limit=20&after=eyJpZCI6MjAsImNyZWF0ZWRBdCI6IjIwMjUtMDMtMTVUMDk6MzA6MDBaIn0 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
# Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{ "id": 21, "name": "Monitor Stand", "price": 59.00 },
... 19 more items ...
],
"pagination": {
"limit": 20,
"hasNextPage": true,
"hasPrevPage": true,
"nextCursor": "eyJpZCI6NDAsImNyZWF0ZWRBdCI6IjIwMjUtMDMtMTRUMTI6MDA6MDBaIn0",
"prevCursor": "eyJpZCI6MjEsImNyZWF0ZWRBdCI6IjIwMjUtMDMtMTVUMDg6MDA6MDBaIn0"
}
}
Pros
- Consistent performance regardless of dataset position
- No skipped or duplicated items when data changes
- Works efficiently with real-time data streams
- No expensive
COUNT(*)queries needed
Cons
- Cannot jump to an arbitrary page
- Cannot display "Page X of Y" without extra queries
- More complex client-side implementation
- Cursors are opaque — harder to debug
Offset vs Cursor Comparison
| Criteria | Offset Pagination | Cursor Pagination |
|---|---|---|
| Performance | Degrades at high offsets | Constant time regardless of position |
| Consistency | Items can shift between pages | Stable — no duplicates or gaps |
| Implementation | Simple — maps to SQL OFFSET | Moderate — requires indexed sort key |
| Random Access | Yes — jump to any page | No — forward/backward only |
| Total Count | Typically included | Usually omitted (expensive) |
| Best For | Admin panels, small datasets, reports | Feeds, timelines, large or real-time data |
Link Header Pagination (RFC 8288)
Instead of pagination metadata in the response body, some APIs (like GitHub) use the standard Link header. This keeps the response body clean and follows the HATEOAS principle.
# Response with Link header pagination
HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://api.example.com/products?limit=20&after=eyJpZCI6NDB9>; rel="next",
<https://api.example.com/products?limit=20&before=eyJpZCI6MjF9>; rel="prev",
<https://api.example.com/products?limit=20>; rel="first"
[
{ "id": 21, "name": "Monitor Stand", "price": 59.00 },
{ "id": 22, "name": "Cable Management Kit", "price": 24.00 },
... 18 more items ...
]
If you use Link headers, also include an Access-Control-Expose-Headers: Link CORS header so browser-based clients can read it. Without this, the Link header is invisible to JavaScript.
Filtering
Filtering lets clients narrow results to exactly what they need, reducing payload size and database load. Use query parameters with clear, consistent naming.
# Simple equality filters
GET /api/v1/products?status=active&category=electronics
GET /api/v1/users?role=admin&department=engineering
# Range filters with suffixes
GET /api/v1/orders?created_after=2024-01-01&created_before=2024-12-31
GET /api/v1/products?price_min=10&price_max=100
# Multiple values (comma-separated)
GET /api/v1/products?status=active,featured&tags=wireless,ergonomic
# Search / full-text filter
GET /api/v1/products?search=mechanical+keyboard
# Combined example — active electronics under $100, created this year
GET /api/v1/products?status=active&category=electronics&price_max=100&created_after=2024-01-01&limit=20 HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{ "id": 42, "name": "Wireless Mouse", "price": 45.00, "status": "active", "category": "electronics" },
{ "id": 78, "name": "USB-C Hub", "price": 39.00, "status": "active", "category": "electronics" }
],
"pagination": { "limit": 20, "offset": 0, "totalItems": 2, "totalPages": 1 },
"filters": { "status": "active", "category": "electronics", "priceMax": 100, "createdAfter": "2024-01-01" }
}
Sorting
Sorting controls the order of results. Two common patterns exist — choose one and stay consistent across your API.
Explicit Parameter Style
# Separate sort and order params
GET /api/v1/products
?sort=createdAt
&order=desc
GET /api/v1/products
?sort=price
&order=asc
# Multiple sort fields
GET /api/v1/products
?sort=category,price
&order=asc,desc
Prefix Style (Compact)
# Prefix with - for descending
GET /api/v1/products
?sort=-createdAt
GET /api/v1/products
?sort=price
# Multiple sort fields
GET /api/v1/products
?sort=-category,price
Always define a default sort order (typically -createdAt for most resources). Document which fields are sortable — not every field needs to support sorting. Only allow sorting on indexed database columns.
Field Selection (Sparse Fieldsets)
Let clients request only the fields they need. This reduces payload size, especially for resources with many properties or nested objects.
# Request only specific fields
GET /api/v1/users?fields=id,name,email HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGci...
# Response — only requested fields returned
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": [
{ "id": 1, "name": "Ada Lovelace", "email": "ada@example.com" },
{ "id": 2, "name": "Grace Hopper", "email": "grace@example.com" }
]
}
# Without field selection (full response)
GET /api/v1/users HTTP/1.1
# Each user object would include all fields:
# id, name, email, role, bio, avatarUrl, department,
# createdAt, updatedAt, lastLoginAt, preferences, ...
REST Field Selection
Uses ?fields= query parameter. Simple to implement. Limited to top-level fields in most implementations.
GET /api/v1/users?fields=id,name
GraphQL Approach
Built-in field selection with nested objects. More powerful but requires a GraphQL schema and runtime.
query {
users {
id
name
posts { title }
}
}
Error Handling
When things go wrong, tell the client exactly what happened, why, and what to do next. Structured errors are a feature, not an afterthought.
RFC 9457 — Problem Details for HTTP APIs
RFC 9457 (formerly RFC 7807) defines a standard JSON format for error responses. Using this standard means clients only need to learn one error structure, and tooling can parse errors automatically.
# Content-Type for Problem Details responses
Content-Type: application/problem+json
# Basic structure
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The request body contains invalid fields.",
"instance": "/api/v1/users/42"
}
Problem Details Fields
Standard Members
The type field is meant to be a stable, machine-readable identifier. Point it at a documentation page that explains the error, its causes, and how to fix it. Clients can use this URI to dispatch error handling logic.
Error Response Examples
400 — Validation Error
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 400,
"detail": "The request body contains 2 invalid fields.",
"instance": "/api/v1/users",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address.",
"rejected": "not-an-email"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Must be between 13 and 150.",
"rejected": -5
}
]
}
401 — Authentication Error
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
WWW-Authenticate: Bearer error="invalid_token"
{
"type": "https://api.example.com/errors/token-expired",
"title": "Authentication Required",
"status": 401,
"detail": "The access token expired at 2025-03-15T10:30:00Z. Request a new token using your refresh token.",
"instance": "/api/v1/users/me",
"expiredAt": "2025-03-15T10:30:00Z"
}
403 — Authorization Error
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/insufficient-permissions",
"title": "Insufficient Permissions",
"status": 403,
"detail": "You need the 'admin:write' permission to delete users.",
"instance": "/api/v1/users/42",
"requiredPermissions": ["admin:write"],
"currentPermissions": ["user:read", "user:write"]
}
404 — Not Found
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/resource-not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "No user found with ID 'usr_9999'.",
"instance": "/api/v1/users/usr_9999",
"resourceType": "User",
"resourceId": "usr_9999"
}
429 — Rate Limit Exceeded
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1710504000
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "You have sent 1001 requests in the current hour window. Retry after 45 seconds.",
"instance": "/api/v1/products",
"retryAfter": 45,
"limit": 1000,
"windowSeconds": 3600
}
500 — Internal Server Error
HTTP/1.1 500 Internal Server Error
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred. Our team has been notified. Reference this incident ID when contacting support.",
"instance": "/api/v1/orders/789",
"incidentId": "inc_a1b2c3d4e5f6"
}
Never expose stack traces, internal file paths, database queries, or server configuration in 5xx error responses. Attackers use this information for reconnaissance. Log the details server-side and return only the incident ID to the client.
Error Codes vs Messages
Always include both a machine-readable code and a human-readable message in error responses. They serve different audiences.
Machine-Readable Codes
Use UPPER_SNAKE_CASE for error codes. Clients switch on these values programmatically. Codes must be stable across API versions.
# Machine-readable codes
"code": "VALIDATION_ERROR"
"code": "INVALID_FORMAT"
"code": "RESOURCE_NOT_FOUND"
"code": "TOKEN_EXPIRED"
"code": "RATE_LIMIT_EXCEEDED"
"code": "INSUFFICIENT_PERMISSIONS"
Human-Readable Messages
Messages are for developers reading logs and users seeing error UI. They can change between versions and should be as helpful as possible.
# Human-readable messages
"message": "The request body
contains invalid fields."
"message": "Must be a valid
email address."
"message": "No user found
with ID 'usr_9999'."
"message": "The access token
expired. Please refresh."
Retry-After Header
The Retry-After header tells the client when to try again. It supports two formats. Include it with all 429 responses and optionally with 503 Service Unavailable.
# Format 1 — Delay in seconds (preferred)
Retry-After: 30
# Format 2 — HTTP date (absolute time)
Retry-After: Sun, 16 Mar 2025 10:30:00 GMT
# Full 429 response with Retry-After
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json
Retry-After: 30
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Try again in 30 seconds."
}
# 503 during maintenance with Retry-After
HTTP/1.1 503 Service Unavailable
Retry-After: Sun, 16 Mar 2025 12:00:00 GMT
{
"type": "https://api.example.com/errors/maintenance",
"title": "Service Unavailable",
"status": 503,
"detail": "Scheduled maintenance in progress. Expected completion at 12:00 UTC."
}
Partial Success — 207 Multi-Status
When a batch operation succeeds for some items and fails for others, return 207 Multi-Status with individual status codes for each item. This avoids the ambiguity of returning 200 (hiding failures) or 400 (hiding successes).
# Batch request — create multiple users
POST /api/v1/users/batch HTTP/1.1
Content-Type: application/json
{
"items": [
{ "name": "Ada Lovelace", "email": "ada@example.com" },
{ "name": "Grace Hopper", "email": "invalid-email" },
{ "name": "Alan Turing", "email": "alan@example.com" }
]
}
# Response — 207 Multi-Status
HTTP/1.1 207 Multi-Status
Content-Type: application/json
{
"summary": {
"total": 3,
"succeeded": 2,
"failed": 1
},
"results": [
{
"index": 0,
"status": 201,
"data": { "id": 101, "name": "Ada Lovelace", "email": "ada@example.com" }
},
{
"index": 1,
"status": 400,
"error": {
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"detail": "Invalid email format.",
"errors": [{ "field": "email", "code": "INVALID_FORMAT", "message": "Must be a valid email address." }]
}
},
{
"index": 2,
"status": 201,
"data": { "id": 102, "name": "Alan Turing", "email": "alan@example.com" }
}
]
}
Always include a summary object with total, succeeded, and failed counts so clients can quickly check if they need to inspect individual results. The index field maps each result back to the original request item.
Error Handling Best Practices
Use Consistent Structure
Every error response from your API should follow the same format. Clients should never have to guess whether the error is in error.message, errors[0], or detail.
Be Specific, Not Vague
Say "The email field must be a valid email address" not "Bad request." Say "User usr_9999 not found" not "Not found." Specific errors save hours of debugging.
Return All Validation Errors
Do not return only the first validation error. Collect all invalid fields and return them together so the client can fix everything in one round trip.
Log Correlation IDs
Include an incidentId or requestId in error responses. This lets support teams trace client-reported errors back to server logs without exposing internal details.
Document Every Error Code
Your API docs should list every error type and code your API can return, with causes and remediation steps. The type URI should link to this documentation.
Distinguish Client vs Server
4xx errors mean the client did something wrong and should fix the request. 5xx errors mean the server failed and the client should retry. Never return 500 for validation errors.
Versioning & Evolution
APIs are contracts. Versioning lets you evolve the contract without breaking every client that depends on it.
Why Version Your API
Every public API will eventually need to change. New features require new fields. Business pivots require structural changes. Without versioning, any change risks breaking existing clients. A versioning strategy gives you a clear upgrade path: old clients keep working while new clients adopt the latest contract.
You need a new version when you must make a breaking change — removing a field, changing a type, or restructuring a response. If you can add features without breaking existing behavior, you do not need a new version.
Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /v1/users |
Most visible, easy to route, cache-friendly, widely adopted | Proliferates base URLs, version embedded in resource identity |
| Query Parameter | /users?version=2 |
Easy to add, optional with default | Easy to forget, harder to route, pollutes query string |
| Header | Accept: application/vnd.api.v2+json |
Clean URLs, follows HTTP content negotiation | Hidden from browser, harder to test, poor caching |
| Hostname | v2.api.example.com |
Complete isolation, independent deployment | DNS management overhead, CORS complexity, certificate costs |
URL path versioning (/v1/, /v2/) is the industry standard. It is explicit, discoverable, and supported by every API gateway, load balancer, and documentation tool out of the box. Use it unless you have a compelling reason not to.
URL Path Versioning in Practice
# Version in the base path, before the resource
GET /api/v1/users # Version 1 — original
GET /api/v2/users # Version 2 — new response format
# Both versions run simultaneously
GET /api/v1/users/42 # Returns flat user object
GET /api/v2/users/42 # Returns user with nested profile
# Routing pattern (Express.js example)
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
# Reverse proxy routing (nginx)
location /api/v1/ {
proxy_pass http://api-v1-service/;
}
location /api/v2/ {
proxy_pass http://api-v2-service/;
}
Version only the major version in the URL. Minor and patch changes should be backward-compatible and deployed in place. If you find yourself at /v14/, your breaking-change threshold is too low.
Breaking vs Non-Breaking Changes
| Change Type | Example | Breaking? |
|---|---|---|
| Remove a field from a response | Drop user.username |
Yes |
| Rename a field | name → full_name |
Yes |
| Change a field's type | id: number → string |
Yes |
| Remove an endpoint | Delete GET /users/search |
Yes |
| Make an optional field required | phone now required on POST |
Yes |
| Change error response structure | Different error JSON format | Yes |
| Add a new optional field to response | Add user.avatar_url |
No |
| Add a new endpoint | Add GET /users/me |
No |
| Add a new optional query parameter | Add ?include=profile |
No |
| Add a new HTTP header | Add X-Request-Cost to responses |
No |
| Add a new enum value | Add "archived" to status values |
No (if clients handle unknown values) |
Deprecation & Sunset
When retiring an API version, use the Deprecation and Sunset headers (RFC 8594) to give clients advance notice. A deprecation means "this still works but will be removed." A sunset means "this is the date it stops working."
# Response headers for a deprecated endpoint
HTTP/1.1 200 OK
Content-Type: application/json
Deprecation: Sun, 01 Jan 2025 00:00:00 GMT
Sunset: Sun, 01 Jul 2025 00:00:00 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"
# Deprecation notice in response body (optional but helpful)
{
"data": [ ... ],
"_deprecation": {
"message": "API v1 is deprecated. Migrate to v2 by July 1, 2025.",
"migration_guide": "https://docs.example.com/migrate-v1-to-v2",
"sunset_date": "2025-07-01"
}
}
Give clients 6 to 12 months between the deprecation announcement and the sunset date. For enterprise APIs, 12 months is standard. Monitor usage of deprecated endpoints and reach out to active consumers directly.
Migration Notices
Every deprecation should come with a migration guide. Include these in your deprecation notices:
What Changed
List every breaking change between the old and new version. Be specific: "the name field was split into first_name and last_name."
Before/After Examples
Show the old request/response alongside the new one. Side-by-side comparisons make migration straightforward.
Timeline & Dates
When was the old version deprecated? When will it be removed? What happens to requests after sunset?
Support Contact
Provide a way for developers to ask questions or request deadline extensions. A migration without support is an ultimatum.
Documentation & Testing
An undocumented API does not exist. An untested API cannot be trusted. Both are non-negotiable for production APIs.
OpenAPI / Swagger
The OpenAPI Specification (formerly Swagger) is the industry standard for describing REST APIs. It defines your endpoints, request/response schemas, authentication, and error formats in a machine-readable YAML or JSON file. Tools like Swagger UI, Redoc, and Postman can consume this spec to generate interactive documentation automatically.
# openapi.yaml — OpenAPI 3.1 example
openapi: 3.1.0
info:
title: Users API
version: 1.0.0
description: Manage user accounts and profiles.
contact:
email: api-support@example.com
servers:
- url: https://api.example.com/v1
description: Production
paths:
/users:
get:
summary: List users
operationId: listUsers
tags: [Users]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: per_page
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: A paginated list of users
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
'401':
$ref: '#/components/responses/Unauthorized'
/users/{id}:
get:
summary: Get a user by ID
operationId: getUser
tags: [Users]
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: The user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
User:
type: object
required: [id, email, name]
properties:
id:
type: integer
example: 42
email:
type: string
format: email
example: ada@example.com
name:
type: string
example: Ada Lovelace
created_at:
type: string
format: date-time
Pagination:
type: object
properties:
page:
type: integer
per_page:
type: integer
total:
type: integer
total_pages:
type: integer
ProblemDetail:
type: object
properties:
type:
type: string
title:
type: string
status:
type: integer
detail:
type: string
responses:
Unauthorized:
description: Missing or invalid authentication
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
Documentation Best Practices
Include Examples for Every Endpoint
Show a complete request (with headers, body, and query parameters) and its response. Developers copy-paste examples; make them work out of the box.
Document All Error Codes
List every error type, status, and code your API can return. Include the cause, an example response body, and remediation steps.
Show Authentication Setup
Walk through the full auth flow: how to get credentials, how to include them in requests, what happens when they expire, and how to refresh them.
Provide SDKs & Code Samples
Offer examples in multiple languages (curl, Python, JavaScript, Go). Auto-generated SDKs from your OpenAPI spec reduce integration time from days to hours.
Use Interactive Documentation
Tools like Swagger UI and Redoc let developers make real API calls from the docs page. A "Try It" button is worth a thousand words of explanation.
Keep Docs in Sync with Code
Generate docs from the OpenAPI spec, and validate the spec in CI. If the spec drifts from the implementation, neither can be trusted.
API Testing Types
| Test Type | Scope | What It Validates | Tools |
|---|---|---|---|
| Unit Tests | Single function / handler | Business logic, validation rules, data transformations | Jest, pytest, JUnit |
| Integration Tests | API endpoint + database | Request routing, serialization, database queries, auth | Supertest, requests, REST Assured |
| Contract Tests | API boundary | Request/response shapes match the spec; no breaking changes | Pact, Schemathesis, Dredd |
| Load Tests | Full system under traffic | Throughput, latency percentiles, error rates at scale | k6, Locust, Gatling, Artillery |
| Security Tests | Attack surface | Auth bypass, injection, rate limit enforcement, data exposure | OWASP ZAP, Burp Suite, Nuclei |
Testing with curl
# GET request with authentication
curl -X GET https://api.example.com/v1/users \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Accept: application/json" \
| jq .
# POST request with JSON body
curl -X POST https://api.example.com/v1/users \
-H "Authorization: Bearer eyJhbGciOi..." \
-H "Content-Type: application/json" \
-d '{
"name": "Ada Lovelace",
"email": "ada@example.com",
"role": "admin"
}'
# Verbose mode — see request/response headers
curl -v -X GET https://api.example.com/v1/users/42
# Test error handling — expect a 404
curl -s -o /dev/null -w "%{http_code}" \
https://api.example.com/v1/users/99999
# Output: 404
# Time the request (connection + transfer)
curl -s -o /dev/null \
-w "connect: %{time_connect}s\ntotal: %{time_total}s\n" \
https://api.example.com/v1/users
Contract Testing
Contract tests verify that an API producer and its consumers agree on the shape of requests and responses. They catch breaking changes before deployment — without spinning up the full system.
| Approach | How It Works | Best For |
|---|---|---|
| Provider-Driven | The API team publishes an OpenAPI spec. Consumers test against it. Tools like Schemathesis fuzz the spec to find violations. | Public APIs, APIs with many consumers you do not control |
| Consumer-Driven | Each consumer writes a "pact" defining what they need. The provider runs all pacts in CI. If a pact fails, the change is blocked. | Microservices where teams own both producer and consumer |
# Schemathesis — fuzz your API against its OpenAPI spec
pip install schemathesis
st run --checks all https://api.example.com/openapi.json
# Pact — consumer-driven contract test (JavaScript)
const { PactV4 } = require('@pact-foundation/pact');
const pact = new PactV4({ consumer: 'WebApp', provider: 'UsersAPI' });
pact.addInteraction({
states: [{ description: 'user 42 exists' }],
uponReceiving: 'a request for user 42',
withRequest: {
method: 'GET',
path: '/api/v1/users/42',
headers: { Accept: 'application/json' },
},
willRespondWith: {
status: 200,
headers: { 'Content-Type': 'application/json' },
body: {
id: 42,
name: 'Ada Lovelace',
email: 'ada@example.com',
},
},
});
Contract tests are the safety net for API evolution. They run in seconds (no real server needed), catch breaking changes in CI, and give both teams confidence to deploy independently.
Best Practices
Patterns, principles, and anti-patterns distilled from decades of API design. The difference between an API people tolerate and one they love.
API Design Checklist
Use Nouns, Not Verbs in URLs
Resources are things: /users, /orders, /invoices. The HTTP method is the verb. Never /getUsers or /createOrder.
Version from Day One
Put /v1/ in your URL path before you launch. Adding versioning later means migrating every client at once — the hardest possible upgrade.
Paginate All Collections
Never return unbounded lists. Use cursor-based or offset pagination with sensible defaults (per_page=20, max=100).
Use Proper HTTP Methods & Status Codes
GET reads. POST creates. PUT replaces. PATCH updates. DELETE removes. Return 201 for creation, 204 for deletion, 404 for missing resources.
Return Structured Errors (RFC 9457)
Every error should include type, title, status, and detail. Consistent error structures let clients build reliable error handling.
Implement Auth & Rate Limiting
Use OAuth 2.0 or API keys. Enforce rate limits with 429 Too Many Requests and Retry-After headers. Protect your API from abuse and overload.
Support Filtering, Sorting, Field Selection
Let clients request only what they need: ?status=active&sort=-created_at&fields=id,name. Less data transferred means faster responses.
Use HTTPS Everywhere
Never serve an API over plain HTTP. TLS protects credentials, tokens, and data in transit. Redirect HTTP to HTTPS. Use HSTS headers.
Document with OpenAPI
Write your OpenAPI spec first, then implement. Generate interactive docs, client SDKs, and contract tests from a single source of truth.
Design for Backward Compatibility
Add fields, do not remove them. Add endpoints, do not rename them. Treat your API like a public promise — breaking it costs trust.
HATEOAS
Hypermedia as the Engine of Application State (HATEOAS) means your API responses include links that tell the client what it can do next. Instead of hard-coding URLs, clients follow links — like clicking through a website.
# GET /api/v1/orders/1234
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": 1234,
"status": "pending",
"total": 89.99,
"currency": "USD",
"created_at": "2025-03-15T10:30:00Z",
"_links": {
"self": { "href": "/api/v1/orders/1234" },
"approve": { "href": "/api/v1/orders/1234/approve", "method": "POST" },
"cancel": { "href": "/api/v1/orders/1234/cancel", "method": "POST" },
"items": { "href": "/api/v1/orders/1234/items" },
"customer": { "href": "/api/v1/customers/567" }
}
}
# After the order is approved, the links change:
{
"id": 1234,
"status": "approved",
"total": 89.99,
"_links": {
"self": { "href": "/api/v1/orders/1234" },
"ship": { "href": "/api/v1/orders/1234/ship", "method": "POST" },
"refund": { "href": "/api/v1/orders/1234/refund", "method": "POST" },
"items": { "href": "/api/v1/orders/1234/items" }
}
}
HATEOAS shines in complex workflow APIs (orders, approvals, state machines) where the available actions depend on the current state. For simple CRUD APIs, it adds overhead without clear benefit. Adopt it when your API has state transitions that clients need to discover dynamically.
Idempotency Keys
Network failures happen. Clients retry. Without idempotency keys, a retried POST /payments might charge the customer twice. An idempotency key lets the server recognize a duplicate request and return the original response instead of processing it again.
# Client generates a UUID and sends it with the request
POST /api/v1/payments HTTP/1.1
Content-Type: application/json
Idempotency-Key: 8a3b9c7e-4d2f-4a1b-9e6c-3f8d7a2b1c0e
{
"amount": 2500,
"currency": "USD",
"customer_id": "cus_abc123",
"description": "Monthly subscription"
}
# First request — server processes and stores the key + response
HTTP/1.1 201 Created
Idempotency-Key: 8a3b9c7e-4d2f-4a1b-9e6c-3f8d7a2b1c0e
{
"id": "pay_xyz789",
"amount": 2500,
"status": "succeeded"
}
# Retry with same key — server returns cached response (no double charge)
HTTP/1.1 201 Created
Idempotency-Key: 8a3b9c7e-4d2f-4a1b-9e6c-3f8d7a2b1c0e
{
"id": "pay_xyz789",
"amount": 2500,
"status": "succeeded"
}
Store idempotency keys server-side with the response for 24 to 48 hours. Use UUIDs (v4) for key generation. This pattern is used by Stripe, PayPal, and most payment APIs.
Caching
Proper caching reduces server load, lowers latency, and saves bandwidth. HTTP has built-in caching mechanisms — use them instead of inventing your own.
# Server responds with ETag (a fingerprint of the response)
GET /api/v1/users/42 HTTP/1.1
HTTP/1.1 200 OK
ETag: "a1b2c3d4e5f6"
Cache-Control: private, max-age=60
Content-Type: application/json
{ "id": 42, "name": "Ada Lovelace", "email": "ada@example.com" }
# Client sends conditional request with If-None-Match
GET /api/v1/users/42 HTTP/1.1
If-None-Match: "a1b2c3d4e5f6"
# Data hasn't changed — server returns 304 with no body
HTTP/1.1 304 Not Modified
ETag: "a1b2c3d4e5f6"
Cache-Control: private, max-age=60
# Common Cache-Control directives
Cache-Control: public, max-age=3600 # CDN + browser cache for 1 hour
Cache-Control: private, max-age=60 # Browser only, 1 minute
Cache-Control: no-cache # Always revalidate with server
Cache-Control: no-store # Never cache (sensitive data)
Webhooks
Polling wastes resources. Webhooks flip the model: instead of the client asking "did anything change?" every few seconds, the server pushes events to the client when something happens.
# 1. Client registers a webhook endpoint
POST /api/v1/webhooks HTTP/1.1
Content-Type: application/json
{
"url": "https://myapp.example.com/hooks/orders",
"events": ["order.created", "order.shipped", "order.refunded"],
"secret": "whsec_abc123..."
}
# 2. When an event occurs, the server POSTs to the registered URL
POST https://myapp.example.com/hooks/orders HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: sha256=abc123...
X-Webhook-ID: evt_789xyz
X-Webhook-Timestamp: 1710500000
{
"event": "order.shipped",
"timestamp": "2025-03-15T14:00:00Z",
"data": {
"order_id": 1234,
"tracking_number": "1Z999AA10123456784",
"carrier": "UPS"
}
}
# 3. Client verifies the signature and responds with 200
HTTP/1.1 200 OK
Always sign webhook payloads with HMAC-SHA256 using a shared secret. The receiver must verify the signature before processing the event. Include a timestamp to prevent replay attacks. Retry failed deliveries with exponential backoff.
Anti-Patterns
| Anti-Pattern | Example | Why It's Wrong | Do This Instead |
|---|---|---|---|
| Verbs in URLs | POST /getUsers |
HTTP methods are the verbs; URLs should be nouns | GET /users |
| 200 for errors | 200 { "error": "not found" } |
Clients, proxies, and monitoring tools rely on status codes | 404 with Problem Details body |
| No versioning | /api/users (no version) |
Any structural change breaks every client at once | /api/v1/users |
| Deep nesting | /users/1/orders/2/items/3/reviews |
Hard to maintain, tightly couples resources | /reviews?order_item_id=3 |
| Returning arrays as root | [{ "id": 1 }, ...] |
Cannot add metadata (pagination, links); JSON hijacking risk | { "data": [...], "pagination": {...} } |
| Ignoring Accept header | Always returning JSON regardless | Violates HTTP content negotiation | Return 406 Not Acceptable for unsupported types |
| Exposing database IDs | auto_increment integers in URLs |
Reveals record count, enables enumeration attacks | Use UUIDs or prefixed IDs (usr_abc123) |
| No rate limiting | Unlimited requests allowed | One misbehaving client can take down the entire API | 429 + Retry-After header |