Building Blocks11 min read

API Design

The contract between your system and the world
scope:Building Blockdifficulty:Beginner-Intermediate

What is an API?

An API (Application Programming Interface) is like a menu at a restaurant. You don't walk into the kitchen and cook your own food. Instead, you look at the menu, pick what you want, and the kitchen makes it for you. The menu is the interface — it tells you what's available, what you need to provide ("how would you like your steak cooked?"), and what you'll get back.

In software, an API defines how two systems talk to each other. Your mobile app talks to your backend through an API. Your backend talks to Stripe's payment system through an API. APIs are everywhere.

Good API design matters because:

  • It's a contract. Once developers start using your API, changing it breaks their code.
  • It affects performance. Chatty APIs (many small calls) are slower than well-designed ones.
  • It shapes the developer experience. A confusing API means frustrated developers and more support tickets.
Client-server request-response model

REST: The Most Common API Style

REST (REpresentational State Transfer) is the most popular API style on the web. It uses standard HTTP methods to perform operations on resources.

The key principles:

  • Resources: Everything is a resource with a URL. Users live at /users, a specific user at /users/42, their posts at /users/42/posts.
  • HTTP Methods: Use the right verb for the right action:

GET /users/42 — Read user 42
POST /users — Create a new user
PUT /users/42 — Replace user 42 entirely
PATCH /users/42 — Update specific fields of user 42
DELETE /users/42 — Delete user 42

  • Stateless: Each request contains all the information needed. The server doesn't remember previous requests (a key principle for scalability).
  • JSON: Most REST APIs use JSON for request/response bodies.
REST verbs — GET, POST, PUT, DELETE

HTTP Status Codes

Status codes tell the client what happened. Think of them as the server's facial expressions:

2xx — Success (smiling)

  • 200 OK — Everything worked. Here's your data.
  • 201 Created — New resource created successfully.
  • 204 No Content — Success, but nothing to return (common for DELETE).

3xx — Redirection (pointing somewhere else)

  • 301 Moved Permanently — This resource has a new URL forever.
  • 304 Not Modified — Use your cached version, nothing changed.

4xx — Client Error (you messed up)

  • 400 Bad Request — Your request doesn't make sense.
  • 401 Unauthorized — Who are you? (Not authenticated.)
  • 403 Forbidden — I know who you are, but you can't do this.
  • 404 Not Found — This resource doesn't exist.
  • 429 Too Many Requests — Slow down! You're being rate limited.

5xx — Server Error (we messed up)

  • 500 Internal Server Error — Something broke on our end.
  • 502 Bad Gateway — The server behind us is broken.
  • 503 Service Unavailable — We're overloaded or in maintenance.
HTTP status codes at a glance

RESTful API Design Example

from flask import Flask, jsonify, request
app = Flask(__name__)
# In-memory "database"
users = {
1: {"id": 1, "name": "Alice", "email": "[email protected]"},
2: {"id": 2, "name": "Bob", "email": "[email protected]"},
}
next_id = 3
# GET /users — List all users (with pagination)
@app.route("/api/v1/users", methods=["GET"])
def list_users():
page = request.args.get("page", 1, type=int)
per_page = request.args.get("per_page", 10, type=int)
all_users = list(users.values())
start = (page - 1) * per_page
end = start + per_page
return jsonify({
"data": all_users[start:end],
"page": page,
"per_page": per_page,
"total": len(all_users)
}), 200
# GET /users/:id — Get a specific user
@app.route("/api/v1/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
user = users.get(user_id)
if not user:
return jsonify({"error": "User not found"}), 404
return jsonify({"data": user}), 200
# POST /users — Create a new user
@app.route("/api/v1/users", methods=["POST"])
def create_user():
global next_id
body = request.get_json()
if not body or "name" not in body or "email" not in body:
return jsonify({"error": "name and email are required"}), 400
user = {"id": next_id, "name": body["name"], "email": body["email"]}
users[next_id] = user
next_id += 1
return jsonify({"data": user}), 201
# DELETE /users/:id — Delete a user
@app.route("/api/v1/users/<int:user_id>", methods=["DELETE"])
def delete_user(user_id):
if user_id not in users:
return jsonify({"error": "User not found"}), 404
del users[user_id]
return "", 204
Output
# GET /api/v1/users?page=1&per_page=10
# → 200 {"data": [...], "page": 1, "total": 2}

# POST /api/v1/users  {"name": "Charlie", "email": "[email protected]"}
# → 201 {"data": {"id": 3, "name": "Charlie", ...}}

# GET /api/v1/users/999
# → 404 {"error": "User not found"}
REST request lifecycle — a GET request flows from client through load balancer to API server, which queries the database and returns a JSON response

API Versioning

APIs evolve. You'll add features, change data formats, deprecate endpoints. But you can't break existing clients that depend on the old behavior. Versioning solves this.

Common strategies:

  • URL path versioning: /api/v1/users, /api/v2/users. The most common and most visible approach.
  • Header versioning: Accept: application/vnd.myapi.v2+json. Cleaner URLs but harder to test in a browser.
  • Query parameter: /api/users?version=2. Easy to implement but messy.

URL path versioning wins in practice because it's the most obvious and the easiest to document, cache, and route at the load balancer level.

Pagination

You never want to return 10 million records in one response. Pagination returns data in manageable chunks.

Offset-based: GET /posts?page=3&per_page=20. Simple but has problems: if new posts are added between page requests, you might see duplicates or miss items. Also slow for large offsets (the DB must skip thousands of rows).

Cursor-based: GET /posts?cursor=abc123&limit=20. The cursor is an opaque token (usually an encoded timestamp or ID) pointing to where the last page ended. Much more efficient for large datasets and doesn't have the duplicate/skip problem. This is what Twitter, Facebook, and most modern APIs use.

Pagination with cursors

Rate Limiting

Without rate limiting, a single client could overwhelm your API with millions of requests. Rate limiting caps the number of requests a client can make in a time window.

Rate limit headers tell clients their status:

  • X-RateLimit-Limit: 100 — You can make 100 requests per window
  • X-RateLimit-Remaining: 42 — You have 42 requests left
  • X-RateLimit-Reset: 1640000000 — The window resets at this Unix timestamp

When the limit is exceeded, return 429 Too Many Requests. We'll go deeper into rate limiting algorithms in the Rate Limiter design lesson.

API Gateway — single entry point

Beyond REST: GraphQL and gRPC

GraphQL (developed by Facebook) lets clients request exactly the data they need. Instead of multiple REST endpoints, there's one endpoint where you send a query:

query { user(id: 42) { name, email, posts { title } } }

This solves the over-fetching problem (REST returns all fields even when you only need two) and the under-fetching problem (needing to make multiple REST calls to get related data). Great for mobile apps where bandwidth matters.

gRPC (developed by Google) uses Protocol Buffers (binary format) instead of JSON. It's much faster than REST because binary is smaller than text and it supports HTTP/2 features like streaming and multiplexing. Used for internal service-to-service communication where performance matters more than human readability.

When to use each:

  • REST: Public APIs, simple CRUD apps, when you want maximum compatibility.
  • GraphQL: Complex data requirements, mobile-heavy apps, when clients need flexibility.
  • gRPC: Internal microservice communication, low-latency requirements, streaming data.
GraphQL query resolution — a single request resolves fields from multiple backend services, returning exactly the shape the client requested
Note: Interview tip: In system design interviews, always define the API first — before diving into architecture. List the key endpoints with their HTTP methods, request/response formats, and status codes. This shows structured thinking and gives you a concrete contract to design around. For example: 'POST /api/v1/urls — takes a long URL, returns a short URL with 201 Created.'

Key Metrics

REST JSON serializationn = response size
~0.1-1 ms\(O(n)\)
gRPC Protobuf serializationBinary, 3-10x smaller than JSON
~0.01-0.1 ms\(O(n)\)
GraphQL query parsingDepends on query complexity
~0.5-5 ms\(O(n)\)
Offset pagination (page 1000)DB skips 1000 pages of rows
Slow\(O(offset + limit)\)
Cursor paginationDirect seek to cursor position
Fast\(O(limit)\)

Quick check

Which HTTP method should you use to partially update a user's email without changing their name?

Continue reading

Design a Rate Limiter
Stop the flood before it drowns your servers
Load Balancing
Sharing the work so no server burns out
Design a URL Shortener
Turn long links into tiny ones — at scale
Caching
Keep the good stuff close — skip the slow trip