All topics
General · Learning hub

API Design notes for developers

Master API Design with a curated set of 5 developer notes — core concepts, patterns, and interview prep. Maintained by the DevRecall team.

Save this stack to your DevRecallMore General notes
API Design

REST Principles & URL Design

API Design: REST Principles & URLs REST (Representational State Transfer) is an architectural style for designing networked APIs. A well-designed REST API is in

API Design: REST Principles & URLs

REST (Representational State Transfer) is an architectural style for designing networked APIs. A well-designed REST API is intuitive, consistent, and predictable.

REST Constraints

  • Client-Server: UI and data storage concerns are separated — client doesn't know how data is stored

  • Stateless: every request contains all information needed — no server-side session state

  • Cacheable: responses should indicate whether they can be cached (Cache-Control headers)

  • Uniform Interface: consistent resource identification, manipulation through representations, self-descriptive messages, HATEOAS

  • Layered System: client doesn't know if it's talking to the real server or a proxy/CDN

URL Design

# Resources are nouns, not verbs
❌ GET /getUsers
❌ POST /createUser
✅ GET /users
✅ POST /users

# Hierarchical relationships
GET    /users                    — list all users
POST   /users                    — create a user
GET    /users/{id}               — get one user
PUT    /users/{id}               — replace user (full update)
PATCH  /users/{id}               — partial update
DELETE /users/{id}               — delete user

# Nested resources (use sparingly — max 2 levels deep)
GET  /users/{id}/orders          — user's orders
GET  /users/{id}/orders/{oid}    — specific order
POST /users/{id}/orders          — create order for user

# Actions that don't map to CRUD (use sub-resource or POST)
POST /users/{id}/activate        — activate user
POST /orders/{id}/cancel         — cancel order
POST /emails/send                — send email (not a resource but an action)

HTTP Methods & Idempotency

Method   Idempotent  Safe   Use for
GET      ✅           ✅     Retrieve resource(s)
POST     ❌           ❌     Create resource, trigger action
PUT      ✅           ❌     Replace resource entirely
PATCH    ❌*          ❌     Partial update
DELETE   ✅           ❌     Remove resource
HEAD     ✅           ✅     GET without body (check existence)
OPTIONS  ✅           ✅     CORS preflight, list allowed methods

* PATCH can be made idempotent depending on implementation

Idempotent = calling N times has same result as calling once
Safe = does not modify state

HTTP Status Codes

2xx — Success
  200 OK              — GET success, PATCH/PUT success
  201 Created         — POST success; include Location header with new resource URL
  204 No Content      — DELETE success, PATCH with no body to return

4xx — Client Errors
  400 Bad Request     — malformed JSON, invalid params, validation error
  401 Unauthorized    — not authenticated (wrong/missing token)
  403 Forbidden       — authenticated but not authorized (right token, wrong role)
  404 Not Found       — resource doesn't exist
  409 Conflict        — duplicate resource, optimistic locking conflict
  410 Gone            — permanently deleted (stronger than 404)
  422 Unprocessable   — valid JSON but failed domain validation (preferred over 400 for validation)
  429 Too Many Requests — rate limited; include Retry-After header

5xx — Server Errors
  500 Internal Server Error — unexpected failure
  502 Bad Gateway     — upstream service failed
  503 Service Unavailable — maintenance, overload; include Retry-After
API Design

Auth, Versioning & Pagination

API Design: Auth, Versioning & Pagination Authentication Patterns # API Key — simple, good for server-to-server Authorization: Api-Key sk_live_abc123 X-API-Key:

API Design: Auth, Versioning & Pagination

Authentication Patterns

# API Key — simple, good for server-to-server
Authorization: Api-Key sk_live_abc123
X-API-Key: sk_live_abc123

# Bearer Token (JWT or opaque)
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...

# Basic Auth (avoid — only over HTTPS, not for production APIs)
Authorization: Basic base64(user:password)

# OAuth 2.0 flows:
  Authorization Code (+ PKCE): user-facing apps — redirect to auth server
  Client Credentials:          server-to-server — no user involved
  Device Flow:                 CLIs, smart TVs — poll for approval

JWT Best Practices

  • Use RS256 (asymmetric) over HS256 — public key can be shared for verification

  • Short expiry: access tokens 15min, refresh tokens 7-30 days

  • Never put secrets (passwords, card numbers) in JWT payload — it's base64, not encrypted

  • Validate: signature, expiry (exp), issuer (iss), audience (aud)

  • Token rotation: issue new refresh token when access token is refreshed (detect token theft)

  • Revocation: JWTs are stateless — use a blocklist for critical revocations or use short expiry

API Versioning Strategies

# URL path versioning (most common, most explicit)
/v1/users
/v2/users

# Header versioning (cleaner URLs, harder to test in browser)
Accept: application/vnd.myapi.v2+json
API-Version: 2

# Query parameter (least recommended)
/users?version=2

# Subdomain
v2.api.example.com/users

Best practices:
- Keep v1 working for at least 12-18 months after v2 launch
- Deprecation headers: Sunset: Sat, 01 Jan 2027 00:00:00 GMT
                       Deprecation: true
- Version should change when breaking: renaming fields, changing types, removing endpoints
- Do NOT version for additive changes (new optional fields, new endpoints)

Pagination Patterns

# Offset/limit (simple, but poor performance on large datasets)
GET /users?offset=100&limit=25

Response:
{
  "data": [...],
  "meta": { "total": 1247, "offset": 100, "limit": 25 }
}

# Cursor-based (recommended for large datasets, real-time data)
GET /posts?cursor=eyJpZCI6MTAwfQ&limit=25

Response:
{
  "data": [...],
  "meta": {
    "nextCursor": "eyJpZCI6MTI1fQ",  // base64 of last item's sort key
    "hasMore": true
  }
}

# Page-based
GET /users?page=5&perPage=25

Response:
{
  "data": [...],
  "pagination": { "page": 5, "perPage": 25, "totalPages": 50, "total": 1247 }
}

# Link header (RFC 5988)
Link: <https://api.example.com/users?page=6>; rel="next",
      <https://api.example.com/users?page=50>; rel="last"

Rate Limiting Headers

X-RateLimit-Limit: 1000        — requests allowed per window
X-RateLimit-Remaining: 842     — requests left in current window
X-RateLimit-Reset: 1716768000  — Unix timestamp when window resets

# When rate limited (429):
Retry-After: 60                — seconds until retry allowed
API Design

OpenAPI, Error Handling & Best Practices

API Design: OpenAPI, Errors & Best Practices Error Response Format Consistent error responses make APIs easier to consume. Use RFC 7807 (Problem Details) or a s

API Design: OpenAPI, Errors & Best Practices

Error Response Format

Consistent error responses make APIs easier to consume. Use RFC 7807 (Problem Details) or a similar structure.

// RFC 7807 Problem Details (recommended)
{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation Failed",
  "status": 422,
  "detail": "The request body contains invalid data",
  "instance": "/users/create",
  "errors": [
    { "field": "email", "message": "Must be a valid email address" },
    { "field": "password", "message": "Must be at least 8 characters" }
  ],
  "traceId": "abc123def456"
}

// Simpler format (also common)
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [...],
    "requestId": "req_abc123"
  }
}

OpenAPI 3.1 Specification

openapi: 3.1.0
info:
  title: My API
  version: 1.0.0

paths:
  /users/{id}:
    get:
      summary: Get user by ID
      operationId: getUserById
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: User found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        '404':
          description: User not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'

components:
  schemas:
    User:
      type: object
      required: [id, email]
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        name:
          type: string
          nullable: true
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

Request & Response Design

  • Use camelCase for JSON fields (standard in JS ecosystem); snake_case is common in Python/Ruby APIs

  • Always return the created/updated resource in POST/PUT/PATCH responses — avoids extra GET request

  • Envelope vs. bare: return bare object for single resource, envelope { data, meta } for collections

  • Nullable vs. omitted: be explicit — undefined fields can cause confusion. Either always include or document which fields may be absent.

  • Dates: use ISO 8601 strings ("2026-05-07T14:30:00Z") — not Unix timestamps in JSON

  • IDs: use strings (UUIDs or opaque IDs) not integers — avoids enumeration attacks and handles large IDs

  • Partial responses: ?fields=id,name,email reduces payload for mobile clients

API Design Checklist

  • Resources are nouns, HTTP methods are verbs — no /getUser or /deletePost

  • Consistent naming: plural nouns (/users not /user), consistent casing

  • Proper status codes: 201 for create, 204 for delete, 422 for validation errors

  • Pagination on all list endpoints — never return unbounded lists

  • Rate limiting on all public endpoints — document limits in headers

  • Authentication documented: which endpoints require auth, what format

  • Error responses are consistent and include a requestId for debugging

  • Breaking changes get a new version — document deprecation timeline

  • OpenAPI spec is generated from code (not hand-written) — single source of truth

  • CORS configured for browser clients — explicit origins, not wildcard for authenticated APIs

API Design

GraphQL vs REST vs gRPC vs WebSockets

API Design: Protocol Comparison Decision Matrix Protocol Transport Format Streaming Browser Best for REST HTTP/1.1 JSON ❌ (SSE) ✅ Public APIs, CRUD, simple reso

API Design: Protocol Comparison

Decision Matrix

Protocol    Transport   Format    Streaming  Browser   Best for
REST        HTTP/1.1    JSON      ❌ (SSE)   ✅        Public APIs, CRUD, simple resources
GraphQL     HTTP/1.1    JSON      ✅         ✅        Flexible queries, BFF, multiple clients
gRPC        HTTP/2      Protobuf  ✅ (bi-di) ⚠️*       Internal microservices, high-throughput
WebSockets  TCP         any       ✅ bi-di   ✅        Real-time: chat, live updates, gaming

* gRPC-Web proxy needed for browser support (Envoy / grpc-web package)

REST

  • Strengths: simple, universal, human-readable, excellent tooling (curl, Postman, browsers), CDN-cacheable

  • Weaknesses: over-fetching (too much data) and under-fetching (N+1 requests), no built-in streaming

  • Use when: public API, CRUD operations, clients you don't control, CDN caching matters

  • Versioning needed for breaking changes; hard to evolve without breaking clients

GraphQL

# Client specifies exactly what fields it needs — no over/under-fetching
query GetUserWithPosts {
  user(id: "123") {
    name
    email
    posts(limit: 5) {
      title
      publishedAt
      tags { name }
    }
  }
}

# Mutation
mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    slug
  }
}

# Subscription — real-time via WebSocket
subscription OnNewMessage($roomId: ID!) {
  messageAdded(roomId: $roomId) {
    id
    text
    author { name }
  }
}
  • Strengths: fetch exactly what you need, strongly typed schema, single endpoint, introspection

  • Weaknesses: N+1 queries (use DataLoader), complex caching, overkill for simple APIs, learning curve

  • Use when: BFF (Backend for Frontend), mobile clients needing bandwidth efficiency, multiple client types with different data needs

  • Tools: Apollo Server, Pothos (TypeScript), Strawberry (Python), Hot Chocolate (.NET)

gRPC

// Strongly typed contracts in .proto — code generated for all languages
service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (Order);              // unary
  rpc StreamOrders (StreamRequest) returns (stream Order);          // server streaming
  rpc UploadItems (stream Item) returns (UploadResult);             // client streaming
  rpc Chat (stream Message) returns (stream Message);               // bidirectional
}

message CreateOrderRequest {
  string user_id = 1;
  repeated OrderItem items = 2;
  PaymentMethod payment_method = 3;
}

enum PaymentMethod {
  PAYMENT_METHOD_UNSPECIFIED = 0;
  CREDIT_CARD = 1;
  PAYPAL = 2;
}
  • Strengths: ~5x faster than JSON/REST (binary + HTTP/2 multiplexing), bidirectional streaming, generated type-safe clients for all languages

  • Weaknesses: binary format hard to debug without tooling, browser support limited (grpc-web proxy needed), no caching layer

  • Use when: internal microservice communication, high-throughput data pipelines, polyglot services that need type safety

  • Tools: grpcurl (like curl for gRPC), Kreya, BloomRPC, Buf (proto linting/breaking change detection)

WebSockets & SSE

// WebSocket — full-duplex, client and server can send at any time
const ws = new WebSocket('wss://api.example.com/ws')

ws.onopen = () => ws.send(JSON.stringify({ type: 'subscribe', channel: 'prices' }))
ws.onmessage = (e) => handleMessage(JSON.parse(e.data))
ws.onerror = console.error
ws.onclose = () => scheduleReconnect()

// Server-Sent Events (SSE) — server pushes only, simpler than WebSockets
// Great for: live feeds, notifications, progress updates
const evtSource = new EventSource('/api/notifications', { withCredentials: true })

evtSource.onmessage = (e) => showNotification(JSON.parse(e.data))
evtSource.addEventListener('order-update', (e) => updateOrder(JSON.parse(e.data)))
evtSource.onerror = () => evtSource.close()

// Server side (Node.js)
// res.setHeader('Content-Type', 'text/event-stream')
// res.setHeader('Cache-Control', 'no-cache')
// res.write('data: {"price": 42}\n\n')  // double newline ends a message
// res.write('event: order-update\ndata: {"id": 1}\n\n')
  • WebSockets: use for bidirectional real-time communication (chat, multiplayer, collaborative editing). Requires sticky sessions or pub/sub (Redis) when scaling.

  • SSE: simpler than WebSockets for server-push only. Automatic reconnect built-in. Works over HTTP/1.1. Better for notifications, live dashboards.

  • Long Polling: HTTP request held open until data available — fallback when WebSockets blocked. Poor performance at scale.

  • HTTP/2 Server Push: deprecated and removed from most browsers. Don't use.

API Design

API Security

API Design: API Security OWASP API Security Top 10 identifies the most critical API vulnerabilities. Understanding these is essential for designing safe APIs. O

API Design: API Security

OWASP API Security Top 10 identifies the most critical API vulnerabilities. Understanding these is essential for designing safe APIs.

OWASP API Security Top 10

  • API1 — Broken Object Level Authorization (BOLA/IDOR): always verify the current user owns the resource. Never trust client-provided IDs without checking ownership. GET /orders/123 must verify order 123 belongs to the authenticated user.

  • API2 — Broken Authentication: use short-lived tokens (15min access, 7d refresh), rotate refresh tokens, rate-limit login attempts, use secure cookie attributes (HttpOnly, Secure, SameSite=Strict).

  • API3 — Broken Object Property Level Authorization: never use mass assignment. Whitelist which fields clients can set. PATCH /users/:id must not allow setting role or isAdmin.

  • API4 — Unrestricted Resource Consumption: rate-limit all endpoints, limit request body size, paginate all lists, add query complexity limits in GraphQL, add timeouts to all DB queries.

  • API5 — Broken Function Level Authorization: admin endpoints must check role, not just authentication. Test every privileged endpoint with a non-admin token.

  • API6 — Unrestricted Access to Sensitive Business Flows: limit password resets, OTP requests, payment attempts per user per time window. Detect and block automated flows.

  • API7 — Server Side Request Forgery (SSRF): validate and allowlist URLs when your API fetches external resources. Never pass raw user-supplied URLs to HTTP clients.

  • API8 — Security Misconfiguration: disable debug endpoints in production, remove CORS wildcard (*) for authenticated APIs, set security headers, disable unnecessary HTTP methods.

  • API9 — Improper Inventory Management: document all APIs, decommission old versions, don't expose beta/internal APIs without auth, monitor for shadow APIs.

  • API10 — Unsafe Consumption of APIs: validate and sanitize all data from third-party APIs before using it. Third parties can be compromised.

CORS

# CORS (Cross-Origin Resource Sharing) — browser enforces, server configures

# Simple request: browser sends directly, server must respond with headers
# Preflight: browser sends OPTIONS first for non-simple requests

# Response headers:
Access-Control-Allow-Origin: https://app.example.com    # ✅ specific origin
Access-Control-Allow-Origin: *                          # ❌ never for authenticated APIs
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true                  # required for cookies/auth headers
Access-Control-Max-Age: 86400                           # cache preflight for 24h

# Common mistakes:
# - Reflecting back any Origin header without validation (open CORS)
# - Combining * with Allow-Credentials (browsers reject this)
# - Not handling OPTIONS preflight (returns 404/405)

Authentication & Token Security

# JWT storage — where to keep tokens in the browser

LocalStorage / SessionStorage:
  ✅ Easy to use, accessible from JS
  ❌ XSS vulnerable — any injected script can read it
  ❌ Never store long-lived tokens here

HttpOnly Cookie:
  ✅ Inaccessible to JS — XSS cannot steal it
  ✅ Automatically sent with requests
  ❌ CSRF vulnerable — add CSRF token or use SameSite=Strict

Recommended: HttpOnly Secure SameSite=Strict cookie for refresh tokens
             Bearer header (short-lived access token) from memory (not localStorage)

# Security headers every API should set:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'none'
Referrer-Policy: strict-origin-when-cross-origin

Rate Limiting Strategies

# Fixed Window — count requests in a fixed time window
Limit: 100 requests / 60 seconds
Problem: burst at window boundary (200 requests in 2 seconds straddling the window)

# Sliding Window — rolling window, no boundary burst
More accurate, more memory usage (store per-user timestamps)

# Token Bucket — bucket refills at a rate, burst allowed up to bucket size
bucket_size: 20, refill_rate: 10/second
Allows short bursts (20 at once), then throttles to 10/s sustained

# Leaky Bucket — requests process at fixed rate, excess queued or dropped
Smooth output rate, good for downstream protection

# Implementation tiers:
Per-IP: coarse-grained, spoofable (X-Forwarded-For)
Per-user: after auth, accurate for logged-in users
Per-API-key: for server-to-server, track per key
Per-endpoint: tighter limits on expensive endpoints (search, export)

Input Validation & Injection Prevention

# SQL Injection — NEVER concatenate user input into SQL
❌ db.query("SELECT * FROM users WHERE name = '" + name + "'")
✅ db.query("SELECT * FROM users WHERE name = $1", [name])  // parameterized

# NoSQL Injection (MongoDB)
❌ db.users.find({ username: req.body.username })
   // attacker sends: { "$gt": "" } — returns all users
✅ Validate input type before passing to query
✅ Use schema validation (Zod, Joi) at API boundary

# Command Injection
❌ exec(`convert ${filename} output.jpg`)
✅ execFile('convert', [filename, 'output.jpg'])  // no shell interpolation

# Path Traversal
❌ readFile('./uploads/' + req.params.file)
✅ const safe = path.resolve('./uploads', req.params.file)
   if (!safe.startsWith(path.resolve('./uploads'))) throw new Error('Invalid path')

# General input validation rules:
- Validate at the API boundary (before any processing)
- Reject unknown fields (strict mode in Zod/class-validator)
- Limit string lengths and array sizes
- Validate content types match declared Content-Type header

Keep your API Design knowledge sharp.

Save this stack to your personal DevRecall — add your own notes, track what you're learning, and share what you know with the community.

Get started — free forever