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
- What Is an API?
- REST Principles
- HTTP Methods
- HTTP Status Codes
- RESTful API Best Practices
- API Versioning
- Pagination
- Filtering and Sorting
- Rate Limiting
- GraphQL Overview
- API Documentation
- Key Takeaways
- 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
| Constraint | Meaning | System Design Impact |
|---|---|---|
| Client-Server | Client and server are independent | They can evolve separately |
| Stateless | Each request contains all info needed | Server does not store session state -> easy to scale horizontally |
| Cacheable | Responses declare if they are cacheable | Reduces server load, improves latency |
| Uniform Interface | Consistent resource-based URLs | Predictable, learnable API |
| Layered System | Client does not know if it talks to end server or intermediary | Allows load balancers, CDNs, proxies |
| Code on Demand (optional) | Server can send executable code | Rarely 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
| Rule | Good | Bad |
|---|---|---|
| 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
| Method | Purpose | Idempotent? | Safe? | Request Body |
|---|---|---|---|---|
GET | Read a resource | Yes | Yes | No |
POST | Create a resource | No | No | Yes |
PUT | Replace a resource entirely | Yes | No | Yes |
PATCH | Update part of a resource | No* | No | Yes |
DELETE | Remove a resource | Yes | No | No |
HEAD | Like GET but no body (check existence) | Yes | Yes | No |
OPTIONS | Discover allowed methods (CORS preflight) | Yes | Yes | No |
*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
| Code | Name | When to Use |
|---|---|---|
| 200 | OK | Successful GET, PUT, PATCH, DELETE |
| 201 | Created | Successful POST that created a resource |
| 204 | No Content | Successful DELETE (nothing to return) |
| 301 | Moved Permanently | Resource permanently moved (SEO) |
| 302 | Found | Temporary redirect |
| 304 | Not Modified | Cached version is still valid |
| 400 | Bad Request | Malformed request, validation error |
| 401 | Unauthorized | No authentication credentials provided |
| 403 | Forbidden | Authenticated but lacks permission |
| 404 | Not Found | Resource does not exist |
| 405 | Method Not Allowed | POST on a GET-only endpoint |
| 409 | Conflict | Resource state conflict (duplicate email) |
| 422 | Unprocessable Entity | Valid syntax but semantic errors |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unhandled server exception |
| 502 | Bad Gateway | Upstream service returned invalid response |
| 503 | Service Unavailable | Server overloaded or in maintenance |
| 504 | Gateway Timeout | Upstream 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
| Strategy | Pros | Cons |
|---|---|---|
URL path (/v1/) | Simple, clear, cacheable | URL clutter, harder to deprecate |
Query param (?version=1) | Easy to default | Easy to forget, caching issues |
| Header | Clean URLs | Less discoverable, harder to test |
| No versioning | Simplest | Requires 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
| Feature | Offset | Cursor |
|---|---|---|
| Jump to page N | Yes | No |
| Consistent under writes | No | Yes |
| Performance at deep offsets | Degrades | Constant |
| Implementation | Simple | More complex |
| Best for | Admin dashboards | Infinite 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)
| Tier | Rate Limit | Use Case |
|---|---|---|
| Free | 60 requests/hour | Hobbyists |
| Basic | 1,000 requests/hour | Small apps |
| Pro | 10,000 requests/hour | Production apps |
| Enterprise | Custom | High-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
| Feature | REST | GraphQL |
|---|---|---|
| Endpoints | Many (/users, /posts) | One (/graphql) |
| Data fetching | Fixed structure | Client specifies shape |
| Over-fetching | Common | Eliminated |
| Under-fetching | Common (need multiple calls) | Eliminated |
| Caching | HTTP caching works naturally | More complex (POST requests) |
| File uploads | Straightforward | Needs special handling |
| Learning curve | Low | Medium |
| Tooling | Widespread | Growing (Apollo, Relay) |
| Error handling | HTTP status codes | Always 200, errors in body |
| Best for | Simple CRUD, public APIs | Complex 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
| Practice | Why |
|---|---|
| Include request/response examples | Developers learn by example |
| Document error codes and messages | Reduce support tickets |
| Show authentication flow | First thing developers need |
| Provide SDKs or code samples | Lower integration barrier |
| Maintain a changelog | Let consumers know what changed |
| Use interactive docs (Swagger UI) | Let developers try the API |
Key Takeaways
- REST is resource-oriented — Think in nouns (
/users), not verbs (/getUsers). Use HTTP methods to express actions. - Status codes communicate intent — Use 201 for creation, 204 for deletion, 404 for not found. Do not return 200 for everything.
- Pagination is mandatory for lists — Never return unbounded collections. Cursor-based is better for feeds; offset-based is fine for dashboards.
- Rate limiting protects your system — Always implement it. Return proper headers so clients can adapt.
- API versioning is inevitable — URL path versioning (
/v1/) is the safest default choice. - GraphQL solves over-fetching and under-fetching — But adds complexity. Use it when clients have diverse data needs.
- Good error responses save hours of debugging — Include error codes, messages, field-level details, and request IDs.
- 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:
- Browse restaurants near a location
- View a restaurant's menu
- Add items to cart
- Place an order
- 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