RESTful API Design: Best Practices for Building Web APIs

Learn REST principles, resource naming, HTTP methods, status codes, and best practices. Design clean, maintainable, and scalable RESTful APIs.

published: reading time: 38 min read author: GeekWorkBench updated: June 17, 2026
Quick Summary

REST API design starts with resources and HTTP semantics, then carries through errors, versioning, authentication, caching, and webhooks. This guide compares REST with GraphQL, explains choices such as cursor pagination and idempotency keys, and shows how OpenAPI contracts, rate limits, and observability affect production APIs. Use its examples and checklists to review an API before clients rely on it.

RESTful API Design: Best Practices for Building Web APIs

Introduction

A REST API gives clients a predictable way to work with resources over HTTP. Its contract depends on more than routes: method semantics, status codes, request and response shapes, authentication, and compatibility rules all affect how safely clients can use it.

This guide covers resource design, errors, versioning, pagination, caching, security, and event subscriptions. It also walks through common production failures and trade-offs so you can choose patterns that fit your clients and operational needs.

Resource Naming & URL Design

Resource Naming

Resource names are the foundation of REST API design. Good resource names are:

  • Nouns, not verbs - /users not /getUsers
  • Plural - /users not /user
  • Hierarchical - /users/123/orders for users orders
  • Consistent - Same pattern throughout
# Good resource naming
GET /users              # List users
GET /users/123           # Get user 123
GET /users/123/orders   # Get orders for user 123
POST /users             # Create user
PUT /users/123          # Update user 123
DELETE /users/123       # Delete user 123

# Bad naming (mixing verbs and nouns)
GET /getUsers
GET /getUserById/123
POST /createUser

Nested Resources

Nested resources express relationships:

# A user posts
GET /users/123/posts

# A post comments
GET /posts/456/comments

# Limit nesting depth - typically 2 levels is practical
GET /users/123/posts/789/comments  # Too deep

HTTP Methods & Status Codes

Core HTTP Methods

REST APIs use HTTP methods to indicate actions. The primary methods are:

GET

Retrieves resources. GET requests should be safe (no side effects) and idempotent.

GET /users
GET /users/123

POST

Creates new resources. Each POST request typically creates one resource.

POST /users
Content-Type: application/json

{"name": "Alice", "email": "alice@example.com"}

Response includes the created resource and a 201 status code:

HTTP/1.1 201 Created
Location: /users/124

PATCH

Partially updates a resource. Only the specified fields change.

PATCH /users/123
Content-Type: application/json

{"email": "alice.new@example.com"}

DELETE

Removes a resource.

DELETE /users/123

Idempotency Design Patterns

Idempotency ensures that making the same request multiple times produces the same result. This matters because networks fail — clients may retry requests without knowing if the first attempt succeeded.

Safe Retries with Idempotency Keys

For POST requests (which are not naturally idempotent), clients can include an idempotency key:

POST /payments
Idempotency-Key: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Content-Type: application/json

{"amount": 100, "currency": "USD", "source": "card_123"}

The server stores the idempotency key with the response. If the client retries with the same key, the original response is returned without re-processing:

HTTP/1.1 201 Created
Idempotency-Replayed: true
Location: /payments/999

{"id": 999, "status": "completed", "amount": 100}

Implementing Idempotency Storage

Use a key-value store (Redis, DynamoDB) with TTL:

async function processPayment(payment, idempotencyKey) {
  // Check if already processed
  const cached = await redis.get(`idem:${idempotencyKey}`);
  if (cached) {
    return JSON.parse(cached);
  }

  // Process the payment
  const result = await billing.charge(payment);

  // Cache the result for 24 hours
  await redis.setex(`idem:${idempotencyKey}`, 86400, JSON.stringify(result));

  return result;
}

Idempotency by HTTP Method

Method Natural Idempotency Notes
GET Yes Reading a resource never changes it
PUT Yes Replacing with the same data is idempotent
DELETE Yes Deleting twice returns 404, but resource is gone
PATCH Depends Setting values can be idempotent; increments may not be
POST No Each POST creates a new resource; requires idempotency key

Idempotency Key Best Practices

  • Use UUIDs or similar high-entropy keys — never reuse keys for different requests
  • Store keys with enough TTL to cover client retry windows (at least 24 hours)
  • Return 422 if the request body is invalid (don’t process, but also don’t cache errors)
  • Distinguish between “replayed response” (original result) and “new response” (fresh processing)

Status Codes

REST APIs should use appropriate HTTP status codes:

Success Codes

Code Meaning When to use
200 OK Successful GET, PUT, PATCH
201 Created Successful POST creating resource
204 No Content Successful DELETE

Error Codes

Code Meaning When to use
400 Bad Request Malformed request body
401 Unauthorized Missing or invalid authentication
403 Forbidden Authenticated but not authorized
404 Not Found Resource does not exist
409 Conflict Resource conflict (duplicate, state conflict)
422 Unprocessable Entity Validation errors
500 Internal Server Error Unexpected server error
// Example: Successful response
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com"
}

// Example: Error response
{
  "error": "validation_error",
  "message": "Email is required",
  "details": [
    { "field": "email", "message": "This field is required" }
  ]
}

Request & Response Design

Request Parameters

Content Type

Always specify content type for requests with bodies:

POST /users
Content-Type: application/json

Pagination

For endpoints returning lists, use pagination:

GET /users?page=2&limit=20

Response includes pagination metadata:

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

Filtering and Sorting

Support filtering and sorting through query parameters:

# Filter by status
GET /orders?status=pending

# Filter by multiple fields
GET /orders?status=shipped&customer_id=456

# Sort by field
GET /users?sort=created_at&order=desc

Field Selection

Let clients request specific fields:

GET /users?fields=id,name,email

HATEOAS Implementation

HATEOAS (Hypermedia As The Engine Of Application State) is a REST constraint where clients interact through hypermedia links included in responses. Instead of clients constructing URLs, they follow links the server provides.

Why HATEOAS?

Without HATEOAS, clients hardcode URL patterns. When your API changes, clients break. With HATEOAS, clients discover actions dynamically:

// Response with hypermedia links
{
  "id": 123,
  "name": "Alice Smith",
  "email": "alice@example.com",
  "_links": {
    "self": { "href": "/users/123" },
    "orders": { "href": "/users/123/orders" },
    "cancel": { "href": "/users/123", "method": "DELETE" }
  }
}

A client encountering this response knows it can navigate to orders or cancel the user without hardcoding those URLs.

Use a consistent link structure:

"_links": {
  "self": { "href": "/users/123" },
  "collection": { "href": "/users" },
  "related": {
    "orders": { "href": "/users/123/orders" },
    "payment-methods": { "href": "/users/123/payment-methods" }
  },
  "actions": {
    "update": { "href": "/users/123", "method": "PUT" },
    "deactivate": { "href": "/users/123/deactivate", "method": "POST" }
  }
}

Practical Implementation

For collections, include pagination links:

{
  "data": [...],
  "_links": {
    "self": { "href": "/users?page=2&limit=20" },
    "first": { "href": "/users?page=1&limit=20" },
    "prev": { "href": "/users?page=1&limit=20" },
    "next": { "href": "/users?page=3&limit=20" },
    "last": { "href": "/users?page=10&limit=20" }
  },
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 200
  }
}

Not all APIs need HATEOAS. For simple machine-to-machine integrations, explicit URLs are easier to work with. But for API longevity and self-documenting interfaces, hypermedia is worth the effort.


API Versioning & Error Handling

API Versioning

APIs need to evolve without breaking existing clients. Versioning provides a way to introduce changes.

URL Path Versioning

# Most common approach
GET /v1/users
GET /v2/users

Header Versioning

GET /users
Accept: application/vnd.example.v2+json

Query Parameter Versioning

GET /users?version=2

URL path versioning is the most common because it is explicit and easy to test.

The API Versioning Strategies post covers this topic in depth.

Error Handling

Consistent error responses help clients handle failures:

// Standard error format
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested user does not exist",
    "details": {
      "resource": "user",
      "id": 999
    }
  }
}

Include enough information for debugging but do not leak sensitive details.

Handling Validation Errors

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "name", "message": "Name must be at least 2 characters" }
    ]
  }
}

Authentication & Security

Authentication

REST itself does not prescribe an authentication mechanism. The protocol is method-agnostic about who is calling and whether they are allowed to. This choice sits with the API designer, and it is one of the highest-stakes decisions in the system: get it wrong and you retrofit pain for years.

Authentication answers the question “who are you?” Authorization answers “what are you allowed to do?” These are different questions, but many APIs conflate them. An authenticated user can still be forbidden from specific actions. A missing token returns 401. A present but insufficient token returns 403.

Three patterns cover most real-world cases: API keys, JWTs, and OAuth 2.0. They are not mutually exclusive — a single API often uses API keys for partner integrations, JWTs for first-party mobile clients, and OAuth 2.0 for delegated access on behalf of end users. Pick the simplest scheme that meets the trust boundary. Authenticate protected endpoints and enforce authorization at resource and action boundaries. Public read routes can remain unauthenticated when their data and abuse controls permit it; define that policy explicitly rather than treating authentication as a performance toggle.

API Keys

Simple but limited. Suitable for server-to-server communication.

GET /users
X-API-Key: your-api-key-here

JWT (JSON Web Tokens)

Stateless tokens with embedded user information. Common for modern applications.

GET /users
Authorization: Bearer eyJhbG...s...

OAuth 2.0

For APIs accessed by third-party applications. More complex but more powerful.

GET /user
Authorization: Bearer access...auth

Documentation & Rate Limiting

API Documentation Deep Dive

OpenAPI (formerly Swagger) is the de facto standard for REST API documentation. A well-structured spec enables client generation, testing tools, and interactive documentation.

A good OpenAPI document does more than describe endpoints. It is the single source of truth from which you generate typed client SDKs in multiple languages, contract tests that catch breaking changes before release, mocks for frontend developers, and reference docs that stay in lockstep with the running service. Treat the spec as a build artifact, not a hand-maintained doc — if a route exists in code but not in the spec, the spec is the bug. The sub-sections below cover the structure of an OpenAPI document, the documentation-first workflow, and how to wire up Swagger UI for interactive exploration.

The key shift: treat the OpenAPI spec as a contract you define before writing code, not documentation you write after the fact. That contract then drives implementation, tests, and client generation. When teams treat it as optional documentation tacked on at the end, they end up with a spec that describes what the API does rather than what it should do — by then, fixing mismatches is already expensive.

OpenAPI Document Structure

openapi: 3.1.0
info:
  title: User Management API
  version: 2.0.0
  description: RESTful API for user lifecycle management

paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        "200":
          description: Paginated user list
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserList"
    post:
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateUser"
      responses:
        "201":
          description: User created
          headers:
            Location:
              schema:
                type: string
              description: URL of created user
        "422":
          description: Validation error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ValidationError"

components:
  schemas:
    UserList:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: "#/components/schemas/User"
        pagination:
          $ref: "#/components/schemas/Pagination"
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
    Pagination:
      type: object
      properties:
        page:
          type: integer
        limit:
          type: integer
        total:
          type: integer
    CreateUser:
      type: object
      required:
        - name
        - email
      properties:
        name:
          type: string
          minLength: 2
        email:
          type: string
          format: email
    ValidationError:
      type: object
      properties:
        error:
          type: object
          properties:
            code:
              type: string
            message:
              type: string
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                  message:
                    type: string

Documentation-First Workflow

The documentation-first approach designs the API contract before writing code:

  1. Write the OpenAPI spec — define endpoints, request/response shapes, error codes
  2. Review with stakeholders — frontend teams, API consumers, product managers
  3. Generate client SDKs — auto-generate libraries for Python, TypeScript, etc.
  4. Implement to spec — code against the agreed contract
  5. Validate — use automated tests to ensure implementation matches spec

This catches design issues before you write a line of implementation code.

Interactive Documentation with Swagger UI

Add Swagger UI as a route in your Express app:

// Express.js example
import swaggerUi from "swagger-ui-express";
import YAML from "yamljs";

const spec = YAML.load("./openapi.yaml");

app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(spec));

Now at https://api.example.com/api-docs, developers can explore endpoints, try requests with live data, and see actual response shapes.


Rate Limiting

Protect your API from abuse by limiting request rates:

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640995200

When clients exceed the limit:

HTTP/1.1 429 Too Many Requests
Retry-After: 3600

Caching Strategies

API Caching Strategies

Caching reduces load, improves latency, and cuts bandwidth costs. REST APIs sit on top of HTTP, which has a complete cache model built in — status codes, headers, validators, and freshness directives. A read-heavy API that skips these primitives leaves significant performance gains on the table for a few lines of header configuration.

Three layers are worth thinking about separately. HTTP caching is what the spec calls for: cache-control directives, ETags, conditional requests, and the Vary header. CDN caching sits in front of the origin and serves cached responses from edge locations close to users. Application caching lives inside the service, typically in Redis or Memcached, and handles data that the HTTP layer cannot represent cleanly. The sub-sections below cover HTTP caching primitives in depth, the harder problem of cache invalidation, and where to apply which layer.

Choosing where to cache depends on who needs the data. User-specific reads like profiles and orders stay in the browser or a private CDN edge. Public shared data like product catalogs belongs at the CDN and in browsers. Complex computed data that changes often (search results, aggregated dashboards) belongs in application cache where you control invalidation. These layers are not mutually exclusive — the same response might flow through all three at once, with each layer handling a different scope.

HTTP Caching Primitives

Cache-Control Header

The Cache-Control header tells clients and intermediaries how to cache responses:

Cache-Control: public, max-age=300, stale-while-revalidate=60

Key directives:

  • public — response can be cached by any cache (not just private)
  • private — response is user-specific, only browser can cache
  • max-age=N — cache for N seconds
  • no-cache — always revalidate before using cached copy
  • no-store — never cache (sensitive data)
  • stale-while-revalidate=N — serve stale content while revalidating in background

ETags for Conditional Requests

ETags provide cache validation without re-downloading content:

# First request - server returns ETag
GET /users/123
HTTP/1.1 200 OK
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Content-Type: application/json

{"id": 123, "name": "Alice"}

# Subsequent request - client sends If-None-Match
GET /users/123
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
HTTP/1.1 304 Not Modified   # No body - use cached version

ETags should change when the resource changes. Use content hashing or version numbers.

HTTP Caching Hierarchy

Layer Scope Typical Duration Use Case
Browser cache User-specific Minutes to days Repeat visits, offline support
CDN Shared Minutes to hours Static assets, API responses
Reverse proxy Shared Configurable Origin protection, cost reduction
Application cache Custom logic Custom Complex data, session tokens

Cache Invalidation

Cache Invalidation Patterns

Invalidation is harder than caching. Four patterns work well:

1. Time-based expiry — simplest, use max-age:

Cache-Control: public, max-age=3600  # Refresh after 1 hour

2. Event-driven invalidation — purge on updates:

# When user updates profile, invalidate related caches
DELETE /cache/users/123
DELETE /cache/users/123/profile
DELETE /cache/users/123/orders

3. Tag-based invalidation (surrogate keys) — tag responses, purge by tag:

# Response tagged with multiple keys
X-Surrogate-Key: user-123 order-456 product-789
# Purge all resources tagged with user-123
POST /purge
Surrogate-Key: user-123

4. Versioned URLs — change URL when content changes:

GET /users/123?v=2   # Cache bust by adding version

Caching for Different Request Types

Endpoint Type Cache Strategy
Public list endpoints Cache-Control: public, max-age=60
User-specific data Cache-Control: private, max-age=300
Real-time data Cache-Control: no-cache (always fresh)
Search results Cache-Control: private, max-age=60 (often user-specific)
Health/check endpoints Cache-Control: no-store (never cache)

Choosing an API Style

When to Use REST

REST is the right choice when you need a simple, well-understood API pattern, HTTP caching helps, or you have multiple clients consuming the same API. It works well when stateless request-response fits your data access patterns, you want easy documentation and discoverability, standard HTTP infrastructure would help, or you’re building CRUD-oriented services.

When Not to Use REST

REST may not be the best fit when clients need different data shapes, you need real-time updates (WebSockets, Server-Sent Events make more sense), your queries are highly complex and deeply nested, you need batched operations that span multiple resources, mobile apps with limited bandwidth need precise data fetching, or your API is tightly coupled to a single client’s needs.

GraphQL vs REST: Trade-off Deep Dive

When choosing between REST and GraphQL, consider these trade-offs:

Factor REST GraphQL Recommendation
Data fetching Multiple endpoints; overfetching common Single request; exact fields needed GraphQL wins for complex UIs
Caching HTTP caching (CDN, browser) works naturally POST requests; requires custom caching layer REST wins for simple caching needs
API evolution Versioning adds complexity Additive changes without versioning GraphQL wins for rapidly evolving APIs
Error handling Standard HTTP status codes 200 OK with errors in body; less explicit REST wins for debuggability
Validation Schema validation at API boundary Type-safe schema; query validation at parse GraphQL wins for developer experience
Monitoring Endpoint-level metrics straightforward Query-level metrics require additional tooling REST wins for operational simplicity
Documentation OpenAPI/Swagger integrates with many tools Schema is the documentation; Self-documenting GraphQL wins for discoverability
Client flexibility Fixed response shapes per endpoint Clients specify needed fields GraphQL wins for heterogeneous clients
Batch operations Multiple requests or custom batch endpoints Single query with multiple operations GraphQL wins
Real-time WebSockets or SSE as separate concerns Subscriptions built into schema GraphQL wins for real-time

Hybrid Patterns

Many teams use both: REST for simple CRUD and authentication, GraphQL for complex data-fetching UI. This is a valid approach — don’t force uniformity where it doesn’t fit.

Webhook and Event Subscription Design

When REST APIs need to notify clients about events (rather than waiting for clients to poll), webhooks provide a push-based alternative. Designing webhook systems requires different considerations than request-response APIs.

Webhook Architecture

sequenceDiagram
    participant C as Client API
    participant S as Your API
    participant Q as Queue
    participant W as Webhook Handler

    S->>S: Resource changes
    S->>Q: Enqueue event
    Q->>W: Process event
    W->>C: POST webhook payload
    Note over C: Client processes event

Payload Design

Deliver structured, predictable payloads:

{
  "event": "user.created",
  "timestamp": "2026-03-22T10:30:00Z",
  "version": "2024-01",
  "data": {
    "id": "usr_abc123",
    "email": "alice@example.com",
    "created_at": "2026-03-22T10:29:58Z"
  },
  "metadata": {
    "correlation_id": "req_xyz789",
    "delivery_attempt": 1
  }
}

Delivery Guarantees

Webhooks are “at-least-once” by nature — network issues can cause duplicate deliveries. Design for that:

// Idempotent webhook handler
async function handleWebhook(payload, signature) {
  const eventId = payload.metadata?.event_id;

  // Check if already processed (idempotency key)
  const processed = await redis.get(`webhook:${eventId}`);
  if (processed) {
    return { status: "duplicate", acknowledged: true };
  }

  // Process the event
  await processEvent(payload);

  // Mark as processed with 7-day TTL
  await redis.setex(`webhook:${eventId}`, 604800, "processed");

  return { status: "processed", acknowledged: true };
}

Signature Verification

Always verify webhook signatures to prevent spoofing:

const crypto = require("crypto");

function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  // Use timing-safe comparison to prevent timing attacks
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(`sha256=${expectedSignature}`),
  );
}

Retry Strategy

When delivery fails, retry with exponential backoff:

Attempt Delay
1 Immediate
2 5 minutes
3 30 minutes
4 2 hours
5 8 hours

After exhausting retries, log the failure and alert operators. Dead letter queues let you replay later.

Webhook Best Practices

  • Send minimal required data — include enough to identify the resource, let clients fetch full data if needed
  • Use stable event IDs — clients use them for deduplication
  • Version your payloads — breaking changes require version bumps
  • Provide test endpoint — let clients configure a test URL that echoes back payloads
  • Include delivery metadata — timestamps, attempt numbers, correlation IDs for debugging

Trade-off Analysis

REST Trade-offs Summary

Designing REST APIs involves navigating several inherent trade-offs. Understanding these helps you make informed decisions for your specific use case.

Statelessness vs. Session State

Aspect Stateless Stateful (with sessions)
Scalability Easy horizontal scaling — any server handles any request Sessions must be shared or sticky; harder to scale
Complexity Each request contains all context Server manages session lifecycle
Reliability Single request failure doesn’t corrupt session state Session loss can corrupt client state
Performance Higher latency — repeated auth/token validation Lower latency — session context pre-loaded

Stateless REST APIs scale effortlessly but push complexity to clients. Stateful APIs reduce client complexity but introduce session management overhead.

Uniform Interface vs. Domain-Specific Optimisation

REST’s uniform interface (standard methods, status codes, link formats) enables generality — any client can consume any REST API. But it can feel constraining when your domain has unique access patterns:

  • REST fits: CRUD-heavy resources, standard resource hierarchies, cacheable data
  • REST strains: Complex multi-step workflows, domain-specific batch operations, real-time streams

Visibility vs. Efficiency

REST’s emphasis on explicit headers, standard methods, and cacheable responses makes intercepting proxies and monitoring easy. But this visibility comes at a cost — HTTP headers add overhead, and the request-response cycle can’t be optimised as tightly as binary protocols.

Versioning Strategies Trade-offs

Strategy Pros Cons
URL path (/v1/users) Simple routing, easy to test, CDN-friendly URL changes on every version bump
Header (Accept: vnd.api.v1+json) Clean URLs, no URL pollution Hard to test in browser, complex routing
Query param (?version=1) Non-invasive, easy to add Caching issues, often ignored by clients

Pagination Trade-offs

Type Pros Cons
Offset (?page=2&limit=20) Simple, user can jump to any page Inconsistent with live data (rows shift), slow on large tables
Cursor (?cursor=abc123) Consistent under concurrent inserts, constant-time Opaque, harder for users to navigate directly
Keyset (?since_id=123) Very fast, great for “newer than X” Limited to sorted-by-id use cases

Production Failure Scenarios

Failure Impact Mitigation
Missing pagination Large datasets cause timeout or memory issues Implement cursor or offset pagination; set reasonable limits
No rate limiting API can be overwhelmed; DoS vulnerability Implement rate limiting per client; return 429 with Retry-After
Inconsistent error responses Clients cannot handle errors gracefully Standardize error format; document all error codes
Version not implemented Breaking changes affect existing clients Version from day one; support multiple versions during transition
N+1 query problem Database overwhelmed with queries Use eager loading; batch related queries; optimize data fetching
Unbounded queries Complex queries slow or crash database Set query complexity limits; implement query timeouts
Missing authentication Unauthorized access to sensitive data Implement authentication on all endpoints; verify permissions
Insufficient validation Invalid data corrupts database Validate all input; use schema validation; return 400 for bad data

Observability Checklist

Observability in REST APIs is the ability to answer questions about system behavior from the outside, using only the signals your service emits. Unlike a monolith where you can attach a debugger to a single process, a REST API sits in a chain of clients, gateways, upstream services, and databases. When something goes wrong, you need to know which request failed, why it failed, and which downstream system was responsible, and you need to know it from logs that already exist.

For REST APIs specifically, observability rests on three pillars: metrics (numeric time-series that show aggregate behavior), logs (discrete event records for individual requests), and traces (causality chains that follow a request across services). Each pillar answers a different kind of question. Metrics answer “is something wrong right now?”: error rates, p99 latency, request volume. Logs answer “what happened to this specific request?”: the actual request, the user, the stack trace. Traces answer “where did the time go across the call chain?”: which downstream service introduced the latency.

The checklist below is the minimum bar for a production REST API, split into metrics, logs, and alerts. Build the signal collection into the request pipeline from day one, with a middleware that emits these signals on every request before the route handler runs. APIs that bolt this on after launch tend to leave the hardest incidents underdocumented, because the requests that fail in interesting ways are the ones no one remembered to instrument.

Metrics

  • Request rate by endpoint, method, and client
  • Response time distribution (p50, p95, p99) per endpoint
  • Error rate by status code and endpoint
  • Active connections or concurrent requests
  • Authentication failures (attempted unauthorized access)
  • Rate limit hits (429 responses)
  • Payload size (request and response)
  • Cache hit ratio (if using caching)

Logs

  • All requests with request ID, method, path, status, duration
  • Authentication and authorization failures with safe client or subject IDs and reason codes; never log credential values
  • Validation errors with field names and codes, without raw submitted values
  • Database query times for slow queries (> 100ms)
  • Rate limiting events with client identifier
  • Error stack traces with correlation IDs

Traces

  • Propagate trace context through the gateway, route handler, database client, and outbound service calls.
  • Add spans for authentication, validation, database work, and external requests; include the route template, status, duration, and retry count.
  • Keep credentials, raw request bodies, and personal data out of span attributes.

Alerts

  • Error rate exceeds 1% for 5 minutes
  • p99 latency exceeds 2 seconds
  • Authentication failures spike (potential attack)
  • Rate limit hits exceed normal threshold
  • Unusual request size or pattern
  • Database query times increasing
  • Memory or CPU usage on API servers

Security Checklist

  • Implement authentication on every protected endpoint
  • Use authorization to verify permissions (not just authentication)
  • Validate and sanitize all input data
  • Use HTTPS for all API endpoints
  • Set appropriate timeouts for all requests
  • Implement rate limiting to prevent abuse
  • Log security events (auth failures, suspicious patterns)
  • Return minimal information in errors (do not leak internal details)
  • Use secure, HttpOnly, SameSite cookies for sessions
  • Implement CORS properly for cross-origin requests
  • Protect against injection attacks (SQL, NoSQL)
  • Validate Content-Type matches expected format
  • Implement HEAD and OPTIONS method security
  • Use API keys or JWT tokens, not passwords in URLs

Compliance considerations

  • Classify the data each endpoint returns, collect only what clients need, and define retention and deletion behavior for API logs and stored records.
  • Keep an auditable record of access to sensitive data, with access controls and retention periods that match the applicable policy.
  • Document where data is processed and stored, and apply the relevant regional or contractual restrictions to requests and backups.
  • Map controls to the regulations and agreements that apply to the service; a generic security checklist does not establish compliance on its own.

Common Pitfalls / Anti-Patterns

Using Verbs in URLs

REST uses nouns for resources, not verbs for actions.

# WRONG - verbs in URLs
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser

# CORRECT - nouns and HTTP methods
GET /api/users
POST /api/users
DELETE /api/users/123

Returning 200 for Errors

Always use appropriate status codes for errors.

# WRONG - 200 for error
HTTP/1.1 200 OK
{"error": "Not found"}

# CORRECT - proper status code
HTTP/1.1 404 Not Found
{"error": "User not found"}

Ignoring Pagination

Never return unbounded lists.

# WRONG - unbounded response
GET /api/users
# Could return millions of users

# CORRECT - paginated response
GET /api/users?page=1&limit=20
# Returns: {"data": [...], "pagination": {"page": 1, "limit": 20, "total": 1000}}

Not Implementing Versioning

APIs need to evolve without breaking clients.

# Version in URL path (most explicit)
GET /v1/users
GET /v2/users

# Version in header (cleaner URLs, harder to test)
GET /users
Accept: application/vnd.example.v2+json

Exposing Internal Details in Errors

Keep error responses informative but not leaky.

// WRONG - exposing internal details
{
  "error": "Database connection failed: mysql://user:pass@host:3306/db"
}

// CORRECT - safe error response
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred"
  }
}

Copy/Paste Checklist

# Test REST endpoint with curl
curl -X GET "https://api.example.com/users?page=1&limit=10" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Accept: application/json"

# Create resource with POST
curl -X POST "https://api.example.com/users" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}'

# Update resource with PATCH
curl -X PATCH "https://api.example.com/users/123" \
  -H "Content-Type: application/json" \
  -d '{"email":"alice.new@example.com"}'

# Delete resource
curl -X DELETE "https://api.example.com/users/123" \
  -H "Authorization: Bearer ..."

# Check API version (if using header versioning)
curl -X GET "https://api.example.com/users" \
  -H "Accept: application/vnd.example.v2+json"

Quick Recap Checklist

Use this checklist when designing or reviewing a REST API:

Resource Design

  • Resources use nouns, not verbs (/users not /getUsers)
  • Resource names are plural consistently (/users not /user)
  • Nested resources express relationships (/users/123/orders)
  • Nesting depth is limited (max 2 levels typically)

HTTP Semantics

  • GET is safe and idempotent (no side effects)
  • POST creates new resources and returns 201 with Location header
  • PUT replaces entire resources (idempotent)
  • PATCH updates only specified fields (partial update)
  • DELETE removes resources (idempotent)
  • DELETE returns 204 on success, 404 if already gone

Response Status Codes

  • 200 for successful GET, PUT, PATCH
  • 201 for successful POST creating a resource
  • 204 for successful DELETE (no body)
  • 400 for malformed requests
  • 401 for missing authentication
  • 403 for authenticated but unauthorized
  • 404 for resources that don’t exist
  • 422 for validation errors
  • 429 when rate limited

Request/Response

  • Pagination on all list endpoints (?page=N&limit=N)
  • Filtering and sorting via query parameters
  • Consistent error response format with machine-readable codes
  • Content-Type header specified for request bodies
  • Location header on 201 responses

Security & Performance

  • HTTPS on all endpoints
  • Authentication on all protected endpoints
  • Rate limiting implemented (X-RateLimit headers)
  • Input validation on all endpoints
  • Cache-Control appropriate for resource type
  • ETag on mutable resources for conditional requests

Documentation

  • OpenAPI spec defined before implementation
  • All error codes documented
  • Versioning strategy defined from day one

Interview Questions

Fundamentals

1. What is the difference between PUT and PATCH HTTP methods in REST API design?
PUT replaces an entire resource — send all fields, even the ones you don't want to change. PATCH applies changes described by the request body. PUT is idempotent; PATCH can be idempotent when it sets values, but operations such as incrementing a field may produce a different result each time.
2. When would you return HTTP 201 instead of HTTP 200?
Use 201 when a POST created something new. The distinction matters: 200 is for requests that succeeded without creating a resource (GET, PUT, DELETE, successful PATCH). 201 tells the client "we made something" and should include a Location header pointing to the new resource.
3. What is HATEOAS and why is it considered a REST constraint?
HATEOAS means responses include links to related actions — a user response tells you about their orders, a payment tells you about the transaction. Clients discover actions by following links rather than hardcoding URLs. This is what Fielding meant by "hypermedia as the engine of application state." It decouples clients from your URL structure so you can change URLs without breaking existing integrations.
4. How do you handle pagination in a REST API?
Query parameters for page and size (e.g., ?page=2&limit=20), with metadata returned alongside the data — current page, total count, total pages. Offset pagination works fine at small scale. Once you're dealing with millions of rows, cursor-based pagination (opaque cursor instead of page number) is faster and handles real-time data better — inserted rows don't create duplicates or gaps.

Design & Architecture

5. What are the trade-offs between URL path versioning and header versioning for APIs?
URL path versioning (/v1/users) is straightforward — visible in the address bar, easy to route at the CDN or API gateway level, simple to test. Downside is URLs that change when versions change. Header versioning keeps URLs clean but you lose visibility (can't test in a browser), routing gets more complex, and caching becomes awkward. In practice, URL path versioning wins because it trades a bit of URL ugliness for operational simplicity.
6. How would you design an idempotent payment API endpoint?
Accept an Idempotency-Key header on POST requests — clients generate a UUID per payment attempt. Store the key with the response in Redis or DynamoDB with a TTL (at least 24 hours). When a request comes in with a known key, return the cached response without re-processing. This means a client can safely retry without charging twice — the network fails, the client retries, you don't double-charge.
7. When would you choose GraphQL over REST for an API?
GraphQL earns its place when you have diverse clients that need different data shapes — a mobile app wanting minimal payloads, a web dashboard wanting everything, a public API with many consumers. The query language is expressive enough that clients can ask for exactly what they need in a single request. REST makes sense when you want straightforward HTTP caching, have simple CRUD operations, or need operational simplicity with off-the-shelf monitoring.
8. How do you prevent N+1 query problems in a REST API?
N+1 happens when fetching a list of resources triggers one query per item for related data. Solutions: eager load with JOINs or batch queries, use a dataloader to batch and cache lookups within a single request, or denormalize related data into the resource. If you're fetching users with orders, don't query users then loop through querying orders — load orders for all users in one query upfront.
9. What information should a REST API error response include?
Three things: a machine-readable error code (VALIDATION_ERROR, NOT_FOUND), a human-readable message, and field-level detail for validation errors. Include a request ID for correlation in logs. Do not leak internal details — no stack traces, database connection strings, or internal URLs. Consistency matters more than completeness: clients need a predictable structure so they can handle errors uniformly.
10. How would you design a REST API endpoint for searching across multiple resource types?
Three common approaches: dedicated search endpoint (GET /search?q=...), query parameter on a proxy resource (GET /resources?type=user&q=...), or a POST-based search for complex queries. The search endpoint should return standardized result objects with type indicators so clients can render appropriately. Include pagination since search can return large result sets. Consider using a query parameter to filter by resource type if combining results.

Advanced & Production

11. How would you implement webhook signature verification?
Compute an HMAC-SHA256 of the raw request body using a shared secret, then compare with the signature in the X-Webhook-Signature header using a timing-safe comparison to prevent timing attacks. If they match, the payload is authentic. Any missing or mismatched signature gets rejected.
12. How do ETags work and why are they useful?
An ETag is a hash or version identifier for a resource's current state. Server sends it with responses. Client stores it and sends it back on subsequent requests via If-None-Match. If the resource hasn't changed, server responds with 304 Not Modified — no body, client uses its cached copy. This saves bandwidth and cuts latency on unchanged resources.
13. Describe a cache invalidation strategy for a REST API serving user-specific data.
User-specific data gets Cache-Control: private, max-age=N — browsers can cache it, CDNs cannot. On updates, you have options: tag-based invalidation (tag responses with user ID, purge by tag), explicit invalidation (delete cache entries when user data changes), or short TTL with stale-while-revalidate (serve stale content while fetching fresh in the background). Time-based expiry is simplest; event-driven invalidation requires more infrastructure.
14. How do you handle partial success in batch operations in a REST API?
Batch operations are tricky because some items may succeed while others fail. Options: return 200 with a result array showing each item's status ({results: [{id: 1, status: "created"}, {id: 2, error: "validation_failed"}]}); or use 207 Multi-Status when different items have different status codes. Include enough detail per item so clients can retry failures. Consider whether batch is the right design — sometimes multiple individual calls with proper error handling is cleaner than trying to handle partial success.
15. How would you design an API for file uploads in a RESTful manner?
Use POST to a resource endpoint with Content-Type: multipart/form-data. The response should include an identifier to retrieve the file later (GET /files/{id}). For large files, consider: multipart upload with chunking for resumability, or pre-signed URLs to bypass your server entirely (client uploads directly to S3/GCS). Set reasonable size limits (enforced with 413 Payload Too Large), validate content types, and scan for malware. Store metadata separately from the file itself so you can query efficiently.
16. What are the advantages and disadvantages of using JSON Patch (RFC 6902) vs simple PATCH with JSON?
JSON Patch (Content-Type: application/json-patch+json) uses a structured format with operations: [{"op": "replace", "path": "/email", "value": "new@example.com"}]. Simple PATCH sends partial JSON directly. JSON Patch is explicit, ordered, and supports add/remove/test operations — better for complex partial updates, concurrent modification testing (test op), and when you need to distinguish between setting null vs omitting a field. Simple PATCH is more intuitive and human-readable for basic partial updates. Both are valid PATCH approaches — pick based on complexity needs.
17. What is the Richardson Maturity Model and how does it relate to REST API design?
The Richardson Maturity Model (RMM) is a way to classify how RESTful an API is across four levels. Level 0: one endpoint, SOAP-like (all operations through POST to a single URL). Level 1: multiple endpoints but only POST. Level 2: multiple endpoints with proper HTTP methods (GET, POST, PUT, DELETE). Level 3: full HATEOAS with hypermedia links in responses. Most "REST APIs" people use today sit at Level 2 — they use HTTP verbs correctly but skip hypermedia. True REST (Fielding's definition) requires Level 3. The RMM is a useful framework for evaluating how closely you're following REST principles.
18. What are the trade-offs between using HTTP caching headers (Cache-Control, ETag) vs application-level caching in REST APIs?
HTTP caching leverages existing infrastructure — CDNs, proxies, browsers all understand Cache-Control and ETags. It's zero-setup at the application level and works across all clients automatically. Application-level caching gives you more control — you can cache complex query results, user-specific data that shouldn't go through HTTP layers, or response transformations. HTTP caching fails when responses are personalized (use private), when you need cache invalidation tied to business events (not just time), or when you're behind a CDN that ignores cache headers. Best practice: use HTTP caching for public, static, or shareable resources; application caching for everything else.
19. How do you handle API backwards compatibility when adding new fields to response payloads?
Adding new fields to responses is backwards-compatible — clients that don't expect the field simply ignore it. This is why REST versioning focuses on breaking changes, not additive ones. Rules: never remove fields (breaking), never rename fields (breaking), never change field types (breaking), never change semantics (breaking). Additive changes are safe: new optional fields, new endpoints, new response properties. When you need to make breaking changes, introduce a new API version. If you need to remove a field, deprecate it first (add a Deprecation header, maybe add a warning in the response), then remove in a future version.
20. What is the difference between synchronous and asynchronous REST API patterns, and when would you use each?
Synchronous REST: client sends request, waits for response, connection held open. This is the default pattern and works for fast operations where you need the result immediately. Asynchronous REST: server accepts the request with 202 Accepted, returns a job/resource ID, client polls or uses webhook for the result. Use async when operations take longer than a few seconds (file processing, report generation, external API calls), when you need to decouple client from server processing time, or when the operation might fail after the client disconnects. Implement async with a job endpoint (POST /reports returns 202 with Location: /reports/123/status), status endpoint, and optional webhook callback.

Further Reading


Conclusion

REST remains the foundation of modern web API design. The principles Fielding defined — resources, uniform interface, statelessness, and hypermedia — provide a framework that scales from simple CRUD services to complex distributed systems. Success with REST comes from disciplined application of HTTP conventions: meaningful status codes, consistent resource naming, proper error formatting, and a versioning strategy you commit to from day one.

Design decisions made at the API boundary ripple through every client and service that depends on it. Investing in clean resource models, idempotent operations, and comprehensive error handling pays dividends across the lifetime of your API. Use the checklists and trade-off analysis in this guide as a starting point, then adapt to your specific domain constraints.

Category

Related Posts

GraphQL vs REST: Choosing the Right API Paradigm

Compare GraphQL and REST APIs, understand when to use each approach, schema design, queries, mutations, and trade-offs between the two paradigms.

#api #graphql #rest

API Versioning: Managing Change Without Breaking Clients

Learn API versioning by comparing URL paths, headers, and query parameters, then plan compatible changes, deprecation, and consumer migrations.

#api #versioning #rest

Rate Limiting: Algorithms for Distributed Systems

Rate limiting protects APIs from abuse. Learn token bucket, sliding window, fixed window algorithms and distributed rate limiting at scale.

#api #rate-limiting #architecture