Episode 9 — System Design / 9.8 — Communication and Data Layer

9.8.b — API Design

Big Picture: An API (Application Programming Interface) is a contract between a client and a server. Good API design makes your system intuitive to use, easy to evolve, and resilient under load. Bad API design creates confusion, breaking changes, and frustrated developers.


Table of Contents

  1. What Is an API?
  2. REST Principles
  3. HTTP Methods
  4. HTTP Status Codes
  5. RESTful API Best Practices
  6. API Versioning
  7. Pagination
  8. Filtering and Sorting
  9. Rate Limiting
  10. GraphQL Overview
  11. API Documentation
  12. Key Takeaways
  13. Explain-It Challenge

What Is an API?

  +-----------+          API Contract          +----------+
  |           |  ------- (request) ----------> |          |
  |  Client   |                                |  Server  |
  | (browser, |  <------ (response) ---------- |          |
  |  mobile,  |                                |          |
  |  service) |  "Give me user #42's profile"  | "Here is |
  |           |  "Here it is: { name: ... }"   |  the data"|
  +-----------+                                +----------+

An API defines:

  • Endpoints — WHERE to send requests (/api/users/42)
  • Methods — WHAT action to perform (GET, POST, PUT, DELETE)
  • Request format — HOW to send data (headers, body, query params)
  • Response format — WHAT comes back (JSON, status codes)
  • Error handling — WHAT happens when things go wrong

REST Principles

REST (Representational State Transfer) is an architectural style, not a protocol. Roy Fielding defined these constraints in his 2000 dissertation:

The Six REST Constraints

ConstraintMeaningSystem Design Impact
Client-ServerClient and server are independentThey can evolve separately
StatelessEach request contains all info neededServer does not store session state -> easy to scale horizontally
CacheableResponses declare if they are cacheableReduces server load, improves latency
Uniform InterfaceConsistent resource-based URLsPredictable, learnable API
Layered SystemClient does not know if it talks to end server or intermediaryAllows load balancers, CDNs, proxies
Code on Demand (optional)Server can send executable codeRarely used in practice

Resources and URIs

REST is resource-oriented. Every entity is a resource with a unique URI.

  GOOD (Resource-oriented)            BAD (Action-oriented)
  ========================            =====================
  GET    /users                       GET  /getUsers
  GET    /users/42                    GET  /getUserById?id=42
  POST   /users                       POST /createUser
  PUT    /users/42                    POST /updateUser
  DELETE /users/42                    POST /deleteUser
  GET    /users/42/orders             GET  /getUserOrders?userId=42

Resource Naming Conventions

RuleGoodBad
Use nouns, not verbs/users/getUsers
Use plural nouns/users/user
Use kebab-case/order-items/orderItems, /order_items
Nest for relationships/users/42/orders/orders?userId=42 (acceptable too)
No trailing slashes/users/users/
No file extensions/users/42/users/42.json

HTTP Methods

MethodPurposeIdempotent?Safe?Request Body
GETRead a resourceYesYesNo
POSTCreate a resourceNoNoYes
PUTReplace a resource entirelyYesNoYes
PATCHUpdate part of a resourceNo*NoYes
DELETERemove a resourceYesNoNo
HEADLike GET but no body (check existence)YesYesNo
OPTIONSDiscover allowed methods (CORS preflight)YesYesNo

*PATCH can be idempotent if implemented carefully, but the spec does not require it.

Idempotent vs Safe

  • Safe = Does not modify server state (GET, HEAD, OPTIONS)
  • Idempotent = Making the same request N times produces the same result as making it once (GET, PUT, DELETE)
  • POST is neither = Each POST may create a new resource
  PUT /users/42  { "name": "Alice" }    <-- Call 10 times: same result (idempotent)
  DELETE /users/42                       <-- Call 10 times: same result (idempotent)
  POST /users    { "name": "Alice" }    <-- Call 10 times: 10 users created (NOT idempotent)

Real-World Example: E-Commerce API

  # Products
  GET    /api/v1/products                    # List all products
  GET    /api/v1/products/101                # Get product 101
  POST   /api/v1/products                    # Create a product
  PUT    /api/v1/products/101                # Replace product 101
  PATCH  /api/v1/products/101                # Update product 101 price
  DELETE /api/v1/products/101                # Delete product 101

  # Orders (nested under user)
  GET    /api/v1/users/42/orders             # List user 42's orders
  POST   /api/v1/users/42/orders             # Create order for user 42
  GET    /api/v1/users/42/orders/7           # Get order 7

  # Cart
  GET    /api/v1/cart                        # Get current cart
  POST   /api/v1/cart/items                  # Add item to cart
  DELETE /api/v1/cart/items/55               # Remove item from cart

HTTP Status Codes

The Five Categories

  1xx - Informational     "Hold on..."
  2xx - Success           "Here you go!"
  3xx - Redirection       "Go over there"
  4xx - Client Error      "You messed up"
  5xx - Server Error      "I messed up"

Must-Know Status Codes

CodeNameWhen to Use
200OKSuccessful GET, PUT, PATCH, DELETE
201CreatedSuccessful POST that created a resource
204No ContentSuccessful DELETE (nothing to return)
301Moved PermanentlyResource permanently moved (SEO)
302FoundTemporary redirect
304Not ModifiedCached version is still valid
400Bad RequestMalformed request, validation error
401UnauthorizedNo authentication credentials provided
403ForbiddenAuthenticated but lacks permission
404Not FoundResource does not exist
405Method Not AllowedPOST on a GET-only endpoint
409ConflictResource state conflict (duplicate email)
422Unprocessable EntityValid syntax but semantic errors
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnhandled server exception
502Bad GatewayUpstream service returned invalid response
503Service UnavailableServer overloaded or in maintenance
504Gateway TimeoutUpstream service timed out

Error Response Format

// GOOD: Structured error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Email address is invalid",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "value": "not-an-email"
      }
    ],
    "request_id": "req_abc123",
    "docs": "https://api.example.com/docs/errors#VALIDATION_ERROR"
  }
}

// BAD: Unhelpful error
{
  "error": "Something went wrong"
}

RESTful API Best Practices

1. Use Consistent Response Envelopes

// Success response
{
  "data": {
    "id": 42,
    "name": "Alice",
    "email": "alice@example.com"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

// List response
{
  "data": [
    { "id": 42, "name": "Alice" },
    { "id": 43, "name": "Bob" }
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "per_page": 20
  }
}

2. Use Appropriate Content Types

# JSON (most common)
Content-Type: application/json

# Form data
Content-Type: application/x-www-form-urlencoded

# File uploads
Content-Type: multipart/form-data

# Accept header (client preference)
Accept: application/json

3. Authentication

# Bearer token (JWT, OAuth)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

# API Key (simpler, less secure)
X-API-Key: sk_live_abc123

# Basic Auth (rarely used in production)
Authorization: Basic base64(username:password)

4. HATEOAS (Hypermedia as the Engine of Application State)

{
  "data": {
    "id": 42,
    "name": "Alice",
    "email": "alice@example.com"
  },
  "links": {
    "self": "/api/v1/users/42",
    "orders": "/api/v1/users/42/orders",
    "avatar": "/api/v1/users/42/avatar"
  }
}

Interview note: HATEOAS is part of the REST maturity model but is rarely implemented in practice. Mentioning it shows deep knowledge.


API Versioning

Strategies

  1. URL PATH VERSIONING (most common)
  =====================================
  GET /api/v1/users
  GET /api/v2/users

  2. QUERY PARAMETER VERSIONING
  ==============================
  GET /api/users?version=1
  GET /api/users?version=2

  3. HEADER VERSIONING
  =====================
  GET /api/users
  Accept: application/vnd.myapi.v1+json
  Accept: application/vnd.myapi.v2+json

  4. NO VERSIONING (evolve carefully)
  ====================================
  Only add fields, never remove or rename
  (GraphQL encourages this approach)

Comparison

StrategyProsCons
URL path (/v1/)Simple, clear, cacheableURL clutter, harder to deprecate
Query param (?version=1)Easy to defaultEasy to forget, caching issues
HeaderClean URLsLess discoverable, harder to test
No versioningSimplestRequires careful evolution

Recommendation for interviews: URL path versioning (/api/v1/) is the safest answer. It is the most widely used and easiest to explain.


Pagination

Offset-Based Pagination

  GET /api/v1/products?page=2&per_page=20

  Response:
  {
    "data": [ ... 20 items ... ],
    "meta": {
      "total": 500,
      "page": 2,
      "per_page": 20,
      "total_pages": 25
    }
  }

Pros: Simple, supports "jump to page 15" Cons: Inconsistent results when data changes between pages. Slow for large offsets (OFFSET 100000 is expensive in SQL).

Cursor-Based Pagination

  GET /api/v1/products?limit=20&cursor=eyJpZCI6MTAwfQ==

  Response:
  {
    "data": [ ... 20 items ... ],
    "meta": {
      "next_cursor": "eyJpZCI6MTIwfQ==",
      "has_more": true
    }
  }

Pros: Consistent results (no skipping/duplicates), fast regardless of position. Cons: Cannot jump to arbitrary page, cursor is opaque.

Comparison

FeatureOffsetCursor
Jump to page NYesNo
Consistent under writesNoYes
Performance at deep offsetsDegradesConstant
ImplementationSimpleMore complex
Best forAdmin dashboardsInfinite scroll, feeds

Real-world usage: Twitter, Facebook, and Instagram all use cursor-based pagination for feeds. GitHub's API supports both.


Filtering and Sorting

Filtering

  # Simple equality
  GET /api/v1/products?category=electronics&in_stock=true

  # Range filters
  GET /api/v1/products?price_min=10&price_max=100

  # Search
  GET /api/v1/products?search=wireless+headphones

  # Multiple values
  GET /api/v1/products?color=red,blue,green

  # Advanced (some APIs use filter syntax)
  GET /api/v1/products?filter[price][gte]=10&filter[price][lte]=100

Sorting

  # Single field sort
  GET /api/v1/products?sort=price           # ascending (default)
  GET /api/v1/products?sort=-price          # descending (prefix with -)

  # Multi-field sort
  GET /api/v1/products?sort=-rating,price   # highest rated first, then cheapest

Field Selection (Sparse Fieldsets)

  # Only return specific fields (reduces payload)
  GET /api/v1/users/42?fields=name,email,avatar_url

  Response:
  {
    "data": {
      "name": "Alice",
      "email": "alice@example.com",
      "avatar_url": "https://..."
    }
  }

Rate Limiting

Rate limiting prevents abuse and protects server resources.

Rate Limit Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000          # Max requests per window
X-RateLimit-Remaining: 847       # Requests left in current window
X-RateLimit-Reset: 1625140800    # Unix timestamp when window resets
Retry-After: 60                  # Seconds to wait (when 429 returned)

Common Rate Limiting Strategies

  1. FIXED WINDOW                          2. SLIDING WINDOW
  ==================                       ==================

  |<--- 1 minute --->|<--- 1 minute --->|  Requests in last 60s from NOW
  |  ||||| |||       | ||| ||||||| |    |  
  | 100 requests max | 100 requests max |  Smoother, no burst at boundaries
  
  3. TOKEN BUCKET                          4. LEAKY BUCKET
  ==================                       ==================
  
  Tokens added at fixed rate               Requests processed at fixed rate
  +--------+                               +--------+
  | Bucket |  refill: 10/sec               | Bucket |  drain: 10/sec
  | [||||] |  max: 100 tokens              | [||||] |  overflow = reject
  +--------+                               +--------+
  Each request consumes 1 token            Smooths out bursts

Rate Limit Response

// HTTP 429 Too Many Requests
{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded. Try again in 60 seconds.",
    "retry_after": 60
  }
}

Tiered Rate Limits (Real World)

TierRate LimitUse Case
Free60 requests/hourHobbyists
Basic1,000 requests/hourSmall apps
Pro10,000 requests/hourProduction apps
EnterpriseCustomHigh-volume partners

GraphQL Overview

GraphQL is a query language for APIs developed by Facebook (2015).

REST vs GraphQL

  REST: Multiple endpoints, fixed responses
  ==========================================

  GET /api/users/42          -> { id, name, email, bio, avatar, ... }
  GET /api/users/42/posts    -> [{ id, title, body, date, ... }, ...]
  GET /api/users/42/friends  -> [{ id, name, avatar, ... }, ...]

  3 requests, lots of unused data (over-fetching)


  GraphQL: Single endpoint, flexible queries
  ===========================================

  POST /graphql
  {
    user(id: 42) {
      name
      posts(first: 5) {
        title
      }
      friends(first: 3) {
        name
      }
    }
  }

  1 request, only the data you need

GraphQL Query Example

# Query
query GetUserDashboard($userId: ID!) {
  user(id: $userId) {
    name
    email
    posts(first: 5, orderBy: CREATED_AT_DESC) {
      edges {
        node {
          title
          likeCount
          commentCount
        }
      }
    }
    notifications(unreadOnly: true) {
      message
      createdAt
    }
  }
}
// Response
{
  "data": {
    "user": {
      "name": "Alice",
      "email": "alice@example.com",
      "posts": {
        "edges": [
          {
            "node": {
              "title": "My First Post",
              "likeCount": 42,
              "commentCount": 7
            }
          }
        ]
      },
      "notifications": [
        {
          "message": "Bob liked your post",
          "createdAt": "2025-01-15T10:30:00Z"
        }
      ]
    }
  }
}

GraphQL Mutations

mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    createdAt
  }
}

# Variables
{
  "input": {
    "title": "New Post",
    "body": "Hello world!",
    "published": true
  }
}

REST vs GraphQL Comparison

FeatureRESTGraphQL
EndpointsMany (/users, /posts)One (/graphql)
Data fetchingFixed structureClient specifies shape
Over-fetchingCommonEliminated
Under-fetchingCommon (need multiple calls)Eliminated
CachingHTTP caching works naturallyMore complex (POST requests)
File uploadsStraightforwardNeeds special handling
Learning curveLowMedium
ToolingWidespreadGrowing (Apollo, Relay)
Error handlingHTTP status codesAlways 200, errors in body
Best forSimple CRUD, public APIsComplex data relationships, mobile apps

Interview guidance: Do not say "GraphQL is better than REST." They solve different problems. REST is better for simple CRUD APIs. GraphQL shines when clients have diverse data needs (e.g., mobile vs desktop).


API Documentation

OpenAPI (Swagger) Example

openapi: 3.0.0
info:
  title: E-Commerce API
  version: 1.0.0

paths:
  /api/v1/products:
    get:
      summary: List products
      parameters:
        - name: category
          in: query
          schema:
            type: string
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: List of products
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Product'
        '401':
          description: Unauthorized

components:
  schemas:
    Product:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        price:
          type: number
          format: float
        category:
          type: string

Documentation Best Practices

PracticeWhy
Include request/response examplesDevelopers learn by example
Document error codes and messagesReduce support tickets
Show authentication flowFirst thing developers need
Provide SDKs or code samplesLower integration barrier
Maintain a changelogLet consumers know what changed
Use interactive docs (Swagger UI)Let developers try the API

Key Takeaways

  1. REST is resource-oriented — Think in nouns (/users), not verbs (/getUsers). Use HTTP methods to express actions.
  2. Status codes communicate intent — Use 201 for creation, 204 for deletion, 404 for not found. Do not return 200 for everything.
  3. Pagination is mandatory for lists — Never return unbounded collections. Cursor-based is better for feeds; offset-based is fine for dashboards.
  4. Rate limiting protects your system — Always implement it. Return proper headers so clients can adapt.
  5. API versioning is inevitable — URL path versioning (/v1/) is the safest default choice.
  6. GraphQL solves over-fetching and under-fetching — But adds complexity. Use it when clients have diverse data needs.
  7. Good error responses save hours of debugging — Include error codes, messages, field-level details, and request IDs.
  8. Document your API from day one — OpenAPI/Swagger is the industry standard for REST APIs.

Explain-It Challenge

Scenario: You are designing the API for a food delivery app (like DoorDash). Define the endpoints for these features:

  1. Browse restaurants near a location
  2. View a restaurant's menu
  3. Add items to cart
  4. Place an order
  5. Track order status in real-time

For each endpoint, specify: method, URL, key request/response fields, and status codes. Also decide: which feature would benefit from WebSockets instead of REST?


Next -> 9.8.c — SQL vs NoSQL