Networking & Communication
Design clean, predictable HTTP APIs. Covers resource naming, HTTP methods, status codes, pagination, versioning, and common anti-patterns.
REST (Representational State Transfer) is the dominant architectural style for web APIs. Despite being widely used, many APIs get the fundamentals wrong — inconsistent naming, misuse of HTTP methods, poor error handling, and pagination that breaks at scale. In system design interviews, demonstrating that you can design a clean, predictable API shows that you think carefully about the interfaces between systems.
REST is built on a few key constraints defined by Roy Fielding in his 2000 dissertation:
Resources are the nouns of your API. They represent the things your system manages — users, orders, products, messages. Good resource naming is the foundation of a well-designed API.
`/users` not `/getUsers`. The HTTP method expresses the action.`/users/123` not `/user/123`. The collection is plural; individual items are accessed by ID.`/user-profiles` not `/userProfiles` or `/user_profiles`. URLs are case-insensitive by convention.`/users/123/orders` — orders belonging to user 123.`/users/123/orders/456/items/789` is too deep. After 2 levels, use a top-level resource: `/order-items/789`.Good:
GET /users
GET /users/123
GET /users/123/orders
POST /users
PUT /users/123
DELETE /users/123/orders/456
Bad:
GET /getUser?id=123 (verb in URL)
POST /createUser (verb in URL)
GET /user/123/order/456 (inconsistent pluralization)
GET /users/123/orders/456/items/789/reviews (too deeply nested)Each HTTP method has specific semantics. Using them correctly makes your API predictable.
┌──────────┬─────────────────────┬─────────────┬────────────┐
│ Method │ Purpose │ Idempotent? │ Safe? │
├──────────┼─────────────────────┼─────────────┼────────────┤
│ GET │ Read a resource │ Yes │ Yes │
│ POST │ Create a resource │ No │ No │
│ PUT │ Replace a resource │ Yes │ No │
│ PATCH │ Partial update │ No* │ No │
│ DELETE │ Remove a resource │ Yes │ No │
└──────────┴─────────────────────┴─────────────┴────────────┘
* PATCH can be made idempotent depending on implementationSafe means the method does not modify server state. Clients can call safe methods without worrying about side effects. Idempotent means calling the method multiple times produces the same result as calling it once.
# PUT replaces the entire resource — idempotent
# Calling this 10 times produces the same result as calling it once
PUT /users/123
{
"name": "Alice",
"email": "alice@example.com",
"role": "admin"
}
# PATCH updates specific fields — the resource is partially modified
PATCH /users/123
{
"role": "admin"
}
# POST creates a new resource — NOT idempotent
# Calling this 10 times creates 10 users
POST /users
{
"name": "Alice",
"email": "alice@example.com"
}Status codes communicate the result of a request. Use them correctly — do not return 200 for everything with an error message in the body.
2xx Success:
200 OK — Request succeeded (general success)
201 Created — Resource was created (POST)
204 No Content — Success with no response body (DELETE)
3xx Redirection:
301 Moved Permanently — Resource has a new URL
304 Not Modified — Cached version is still valid
4xx Client Error:
400 Bad Request — Malformed request (validation error)
401 Unauthorized — Authentication required
403 Forbidden — Authenticated but not authorized
404 Not Found — Resource does not exist
409 Conflict — Conflicting state (duplicate email)
422 Unprocessable — Valid syntax but semantic error
429 Too Many Reqs — Rate limit exceeded
5xx Server Error:
500 Internal Error — Unexpected server failure
502 Bad Gateway — Upstream service failure
503 Unavailable — Server overloaded or in maintenance
504 Gateway Timeout — Upstream service timeoutAlways return structured error responses:
# Consistent error format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Email address is invalid",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
}
]
}
}Any endpoint that returns a list needs pagination. There are two main approaches:
Simple and intuitive. The client specifies which page or offset to start from.
GET /users?offset=20&limit=10
Response:
{
"data": [...],
"pagination": {
"total": 1532,
"offset": 20,
"limit": 10,
"has_more": true
}
}Drawbacks: If items are inserted or deleted between page requests, results can shift — you might see duplicates or miss items. Also, `OFFSET 100000` in SQL is slow because the database scans and discards 100,000 rows.
The server returns an opaque cursor that the client sends to get the next page. The cursor typically encodes the last item's sort key.
GET /users?limit=10&cursor=eyJpZCI6MTIzfQ==
Response:
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTMzfQ==",
"has_more": true
}
}Advantages: Stable results even with concurrent inserts/deletes. Efficient — the database uses an indexed `WHERE id > cursor_id` instead of `OFFSET`.
Drawbacks: Cannot jump to an arbitrary page. No total count (which requires scanning the entire dataset).
# Cursor-based pagination implementation
import base64
import json
def get_users(cursor=None, limit=10):
if cursor:
last_id = json.loads(base64.b64decode(cursor))["id"]
users = db.query(
"SELECT * FROM users WHERE id > %s ORDER BY id LIMIT %s",
[last_id, limit + 1] # Fetch one extra to determine has_more
)
else:
users = db.query(
"SELECT * FROM users ORDER BY id LIMIT %s", [limit + 1]
)
has_more = len(users) > limit
users = users[:limit]
next_cursor = None
if has_more and users:
next_cursor = base64.b64encode(
json.dumps({"id": users[-1].id}).encode()
).decode()
return {"data": users, "next_cursor": next_cursor, "has_more": has_more}Use offset for internal tools, admin dashboards, and small datasets where you need page numbers. Use cursor for production APIs, feeds, timelines, and any dataset that changes frequently or is large.
APIs evolve, and breaking changes are inevitable. Version your API from day one.
Strategies:
1. URL path (most common):
GET /v1/users/123
GET /v2/users/123
2. Query parameter:
GET /users/123?version=2
3. Header:
GET /users/123
Accept: application/vnd.myapi.v2+json
4. Content negotiation:
Accept: application/json; version=2URL path versioning is the most practical: it is visible, cacheable, and easy to route. Header-based versioning is more "RESTful" but harder to test and debug (you cannot share a versioned URL in a browser).
Idempotency is critical for reliability. Networks are unreliable — requests can be sent multiple times due to retries, timeouts, or client bugs. If your API is not idempotent where it should be, duplicate requests cause duplicate side effects (double charges, duplicate orders).
For non-idempotent operations (like POST), the client sends a unique idempotency key. The server stores the result of the first request and returns it for subsequent requests with the same key.
# Server-side idempotency implementation
def create_payment(request):
idempotency_key = request.headers.get("Idempotency-Key")
if not idempotency_key:
return error(400, "Idempotency-Key header required")
# Check if we have already processed this key
cached = redis.get(f"idempotency:{idempotency_key}")
if cached:
return json.loads(cached) # Return the original response
# Process the request
result = payment_service.charge(request.body)
# Store the result (with expiry — keys do not live forever)
redis.setex(
f"idempotency:{idempotency_key}",
86400, # 24 hour expiry
json.dumps(result)
)
return resultStripe's API is the gold standard for idempotency. Every POST request accepts an `Idempotency-Key` header, and the server guarantees that the same key always returns the same result.
HATEOAS (Hypermedia as the Engine of Application State) means including links in responses that tell the client what actions are available. The client does not need to hardcode URLs — it discovers them from the API responses.
# A HATEOAS response for a user resource
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"links": [
{"rel": "self", "href": "/users/123", "method": "GET"},
{"rel": "update", "href": "/users/123", "method": "PUT"},
{"rel": "delete", "href": "/users/123", "method": "DELETE"},
{"rel": "orders", "href": "/users/123/orders", "method": "GET"}
]
}In practice, full HATEOAS is rarely implemented. Most APIs include basic self-links and pagination links, but the full hypermedia approach is uncommon outside of enterprise APIs. Knowing about it shows depth, but do not over-invest in it for interview designs.
Putting it all together for an interview scenario:
Products:
GET /v1/products — List products (paginated, filterable)
GET /v1/products/:id — Get product details
POST /v1/products — Create product (admin)
PUT /v1/products/:id — Update product (admin)
DELETE /v1/products/:id — Remove product (admin)
Cart:
GET /v1/users/:id/cart — Get user's cart
POST /v1/users/:id/cart/items — Add item to cart
PATCH /v1/users/:id/cart/items/:itemId — Update quantity
DELETE /v1/users/:id/cart/items/:itemId — Remove from cart
Orders:
GET /v1/orders — List user's orders (cursor paginated)
POST /v1/orders — Place order (idempotency key required)
GET /v1/orders/:id — Get order details
Search:
GET /v1/products/search?q=laptop&category=electronics&sort=price&order=asc`Retry-After` header when rate limits are exceeded.