← Tech Guides
01

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
Note

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

REQUEST

Content-Type

MIME type of the request body

Content-Type: application/json
REQUEST

Accept

Expected response format

Accept: application/json
REQUEST

Authorization

Authentication credentials

Authorization: Bearer <token>
RESPONSE

Location

URL of newly created resource

Location: /api/users/42
RESPONSE

Cache-Control

Caching directives

Cache-Control: max-age=3600
BOTH

X-Request-ID

Correlation ID for tracing

X-Request-ID: a1b2c3d4-e5f6
02

REST Fundamentals

The architectural constraints, core concepts, and maturity model that define RESTful design.

The 6 REST Constraints

1. Client-Server

Separation UI / Data
Benefit Independent evolution

Client and server are decoupled. The client handles presentation, the server manages data storage. Each can evolve independently.

2. Stateless

Server stores No client state
Each request Self-contained

Every request contains all the information needed to process it. The server does not store session state between requests.

3. Cacheable

Responses declare Cacheability
Result Reduced load

Responses must define themselves as cacheable or non-cacheable. Proper caching eliminates redundant interactions and improves scalability.

4. Uniform Interface

Resources URI-identified
Manipulation Via representations

A consistent, predictable interface: resource identification through URIs, manipulation through representations, self-descriptive messages, and HATEOAS.

5. Layered System

Architecture Hierarchical layers
Client sees Only next layer

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)

Server sends Executable code
Constraint 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": ... }
Tip

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 } } }"
}
03

URL Design

Resource naming, path structure, and query parameter conventions. The address system of your API.

Resource Naming Rules

RULE 01

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
RULE 02

Plural Collections

Use plural nouns for collections. Singular for a specific resource within.

# Collection
GET /api/articles

# Single resource
GET /api/articles/507
RULE 03

Kebab-Case

Use hyphens for multi-word resource names. Lowercase only.

# Good
/api/user-profiles
/api/order-items

# Bad
/api/userProfiles
/api/order_items
RULE 04

No Trailing Slashes

Trailing slashes add no semantic value and create duplicate URIs.

# Good
/api/users

# Bad
/api/users/
RULE 05

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
RULE 06

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

Protocol + Host https://api.example.com
Base path /api/v1
Collection /users
Resource ID /42
Sub-resource /orders
Query params ?status=active&sort=created

Nested Resources: When to Nest vs Flatten

Nest When...

Use nesting

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...

Use top-level

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
Rule of thumb

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
Recommendation

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.

04

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
Note

*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

Semantics Read-only retrieval
Idempotent Yes
Safe Yes — no server state changes
Cacheable Yes — responses should include cache headers
Request body Not allowed
# 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

Semantics Create a new subordinate resource
Idempotent No — each call may create a new resource
Response code 201 Created (with Location header)
# 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"
}
Common Mistake

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

Semantics Full replacement of the resource
Idempotent Yes — same payload always yields same state
Body requirement Must include ALL fields
# 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"
}
Important

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

Semantics Apply partial modifications
Idempotent Depends on patch format
Common formats JSON Merge Patch, JSON Patch

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
Recommendation

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

Semantics Remove the identified resource
Idempotent Yes — result is same whether called 1x or Nx
Typical response 204 No Content (or 200 with body)
# 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"
  }
}
Idempotency Note

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
Best Practice

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.

05

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
Key Distinction

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 vs 422: When to Use Which

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 vs 403: When to Use Which

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
Quick Comparison

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.

06

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"]
  }
}
Note

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

CONVENTION

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
}
CONVENTION

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"
}
CONVENTION

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
Recommendation

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
07

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 }
  ]
}
Warning

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

CHECK

Algorithm (alg)

Verify the alg header matches your expected algorithm. Never accept "none". Use asymmetric algorithms (RS256, ES256) for public APIs.

CHECK

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.

CHECK

Expiration (exp)

Reject tokens where exp is in the past. Allow a small clock skew tolerance (30–60 seconds) for distributed systems.

CHECK

Issuer (iss)

Confirm iss matches the expected authorization server. This prevents tokens issued by a different server from being accepted.

CHECK

Audience (aud)

Confirm aud includes your API's identifier. This prevents a token intended for one API from being replayed against another.

CHECK

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"
Note

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
Warning

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
}
Tip

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

REQUIRED

HTTPS Everywhere

Serve all API endpoints over TLS. Redirect HTTP to HTTPS. Use HSTS headers. No exceptions, even for internal APIs.

REQUIRED

Validate All Input

Validate type, length, format, and range on every field. Reject unexpected fields. Never trust client data regardless of source.

REQUIRED

Parameterized Queries

Never concatenate user input into SQL or NoSQL queries. Use parameterized queries or an ORM to prevent injection attacks.

REQUIRED

Short Token Lifetimes

Access tokens: 15–60 minutes. Refresh tokens: days to weeks with rotation. Revoke tokens on password change or suspicious activity.

REQUIRED

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.

REQUIRED

Monitor API Usage

Log all authentication events. Track anomalous patterns (geographic, volume, timing). Set up alerts for brute force and credential stuffing attempts.

08

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.

Warning

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 ...
]
Note

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
Recommendation

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 }
  }
}
09

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

type URI identifying the error type. Should resolve to documentation. Use "about:blank" if no specific type.
title Short, human-readable summary. Should be the same for every occurrence of this error type.
status HTTP status code (integer). Must match the actual HTTP response status.
detail Human-readable explanation specific to this occurrence. Different each time.
instance URI reference identifying this specific occurrence. Useful for log correlation.
Tip

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"
}
Warning

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" }
    }
  ]
}
Note

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

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

10

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
Recommendation

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 namefull_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"
  }
}
Timeline

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:

INCLUDE

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."

INCLUDE

Before/After Examples

Show the old request/response alongside the new one. Side-by-side comparisons make migration straightforward.

INCLUDE

Timeline & Dates

When was the old version deprecated? When will it be removed? What happens to requests after sunset?

INCLUDE

Support Contact

Provide a way for developers to ask questions or request deadline extensions. A migration without support is an ultimatum.

11

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

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

PRACTICE

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.

PRACTICE

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',
    },
  },
});
Why It Matters

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.

12

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

01

Use Nouns, Not Verbs in URLs

Resources are things: /users, /orders, /invoices. The HTTP method is the verb. Never /getUsers or /createOrder.

02

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.

03

Paginate All Collections

Never return unbounded lists. Use cursor-based or offset pagination with sensible defaults (per_page=20, max=100).

04

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.

05

Return Structured Errors (RFC 9457)

Every error should include type, title, status, and detail. Consistent error structures let clients build reliable error handling.

06

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.

07

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.

08

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.

09

Document with OpenAPI

Write your OpenAPI spec first, then implement. Generate interactive docs, client SDKs, and contract tests from a single source of truth.

10

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" }
  }
}
When to Use

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
Security

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