System Design API Design and REST
An API (Application Programming Interface) is a defined way for two software systems to communicate with each other. It is a contract that specifies what requests a system accepts and what responses it returns. REST (Representational State Transfer) is the most widely used architectural style for building web APIs today.
Think of an API as a restaurant menu. The menu lists what dishes are available, the rules for ordering (specify size, toppings), and what to expect in return. The kitchen's internal workings remain hidden — only the menu interface matters.
Why API Design Matters in System Design
In large systems, different services need to communicate — the payment service talks to the order service, the mobile app talks to the backend, and third-party apps integrate with the platform. APIs are the communication contracts that make all these interactions possible and predictable. Poor API design leads to tight coupling, brittle integrations, and security vulnerabilities.
What Is REST?
REST is an architectural style (not a protocol) for designing networked applications. A REST API uses standard HTTP methods and follows a set of principles that make it simple, stateless, and scalable.
Six Principles of REST
| Principle | What It Means |
|---|---|
| Client-Server | Client and server are separate; client handles UI, server handles data |
| Stateless | Each request contains all information needed; server stores no session state |
| Cacheable | Responses can be cached to improve performance |
| Uniform Interface | Consistent, predictable URL structure and HTTP methods |
| Layered System | Client does not know if it is talking to the final server or a proxy |
| Code on Demand (optional) | Server can send executable code to the client (like JavaScript) |
HTTP Methods in REST
REST uses standard HTTP verbs to define the action being performed:
| HTTP Method | Action | Example |
|---|---|---|
| GET | Retrieve data (read only, no side effects) | GET /users/42 → Get user with ID 42 |
| POST | Create a new resource | POST /users → Create a new user |
| PUT | Replace an entire resource | PUT /users/42 → Replace all fields of user 42 |
| PATCH | Update part of a resource | PATCH /users/42 → Update only the email of user 42 |
| DELETE | Remove a resource | DELETE /users/42 → Delete user 42 |
REST URL Design Best Practices
Good URL design makes an API intuitive and easy to use. Resources use nouns (things), not verbs (actions).
Correct REST URL Design
Resource: Users GET /users → List all users POST /users → Create a new user GET /users/42 → Get user with ID 42 PUT /users/42 → Replace user 42 PATCH /users/42 → Update part of user 42 DELETE /users/42 → Delete user 42 Nested Resources (relationships): GET /users/42/orders → Get all orders for user 42 GET /users/42/orders/7 → Get order 7 belonging to user 42 POST /users/42/orders → Create a new order for user 42
Incorrect REST URL Design
❌ GET /getUser (verb in URL) ❌ POST /createUser (verb in URL) ❌ GET /user_orders_42 (unclear structure) ❌ POST /deleteUser/42 (wrong method for deletion)
HTTP Status Codes
Every API response includes a status code that tells the client whether the request succeeded or failed, and why.
| Code Range | Category | Common Examples |
|---|---|---|
| 2xx | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | Redirect | 301 Moved Permanently, 302 Found |
| 4xx | Client Error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found |
| 5xx | Server Error | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
Real Example:
Request: GET /users/99999 (user does not exist)
Response: HTTP 404 Not Found
Body: { "error": "User not found", "code": "USER_404" }
Request: POST /users (missing required field)
Response: HTTP 400 Bad Request
Body: { "error": "Email is required", "field": "email" }
Request and Response Structure
REST APIs typically use JSON for request and response bodies. A consistent response structure makes the API predictable for all consumers.
Good API Response Structure
Success response:
{
"status": "success",
"data": {
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
}
Error response:
{
"status": "error",
"error": {
"code": "VALIDATION_ERROR",
"message": "Email address is invalid",
"field": "email"
}
}
List response (with pagination):
{
"status": "success",
"data": [ ... ],
"pagination": {
"page": 1,
"limit": 20,
"total": 500,
"totalPages": 25
}
}
API Versioning
APIs change over time. Versioning ensures old clients keep working when the API evolves, while new clients can use new features.
URL Versioning (most common): /api/v1/users ← original version /api/v2/users ← new version with changes Header Versioning: GET /users API-Version: 2 Query Parameter Versioning: GET /users?version=2
URL versioning is the most visible and widely understood approach. Most public APIs (Twitter, Stripe, GitHub) use this approach.
API Authentication
APIs must verify the identity of callers and control access to resources.
API Keys
A long string passed in request headers. Simple but offers no granular permission control.
GET /users Authorization: ApiKey sk_live_abc123xyz
JWT (JSON Web Token)
A signed token containing user identity and permissions. The server validates the signature without needing to query a database for every request.
GET /users/42
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...
JWT structure: header.payload.signature
Payload contains: { "userId": 42, "role": "admin", "exp": 1735689600 }
OAuth 2.0
The standard for delegated authorization. Allows a user to grant a third-party app limited access to their account without sharing their password. Used by "Login with Google" and "Login with GitHub" buttons.
API Pagination
Never return all records at once. A database with 10 million users cannot dump everything in one response. Pagination divides results into manageable chunks.
Offset-Based Pagination
GET /users?page=3&limit=20 → Skip first 40 records (page 3 × limit 20), return next 20 Problem: Slow for large offsets. "Skip 1 million rows" makes the database scan 1 million rows anyway.
Cursor-Based Pagination
GET /users?limit=20&after=cursor_abc123
→ "Give me 20 users after this specific cursor position"
Response includes:
{
"data": [ ... ],
"nextCursor": "cursor_xyz789"
}
Advantage: Fast even for millions of records. Used by Twitter, Facebook, Stripe.
REST vs GraphQL vs gRPC
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Data Fetching | Fixed response structure | Client specifies exact fields needed | Defined by .proto schema |
| Over-fetching | Common | None | None |
| Speed | Fast | Moderate | Very fast (binary protocol) |
| Browser Support | Native | Native | Limited |
| Best For | Public APIs, CRUD services | Mobile apps, flexible queries | Internal microservice communication |
| Example Users | Twitter, GitHub | GitHub v4, Shopify | Google internal services |
API Rate Limiting
Rate limiting restricts how many requests a client can make in a given time window. It protects the API from abuse and ensures fair use.
Rate limit: 100 requests per minute per API key
Response when limit exceeded:
HTTP 429 Too Many Requests
{
"error": "Rate limit exceeded",
"retryAfter": 45 (seconds until limit resets)
}
Response headers always include:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 23
X-RateLimit-Reset: 1735690000
Idempotency in API Design
An idempotent operation produces the same result no matter how many times it executes. This is critical for reliability — if a network failure causes a client to retry a request, a non-idempotent endpoint might create duplicate records.
GET, PUT, DELETE → Always idempotent GET /users/42 → Always returns same user (no side effect) DELETE /users/42 → First call deletes. Repeated calls: user already deleted, same outcome. POST → Not idempotent by default POST /orders → Creates a new order each time. Network retry = duplicate order! Solution: Use Idempotency Keys for POST requests POST /orders Idempotency-Key: unique-key-12345 Server stores the key. If same key arrives again, return original response without creating duplicate.
Summary
REST is the foundation of modern web API design. Well-designed APIs use intuitive URL structures, correct HTTP methods, consistent response formats, and proper status codes. Authentication, versioning, pagination, and rate limiting are non-negotiable in production APIs. Understanding when to use REST versus GraphQL or gRPC depends on the use case — REST remains the best default for public APIs, while gRPC excels for internal high-performance service-to-service communication.
