System Design Article
REST API Design & Best Practices
Difficulty: Easy
REST (Representational State Transfer) is the dominant architectural style for building web APIs. Nearly every system you design in an interview will have a REST API as the interface between clients and servers. This lesson covers REST principles, URL design conventions, request/response patterns, pagination, versioning, error handling, and the trade-offs that separate a good API from a bad one.
REST API Design & Best Practices
REST (Representational State Transfer) is the dominant architectural style for building web APIs. Nearly every system you design in an interview will have a REST API as the interface between clients and servers. This lesson covers REST principles, URL design conventions, request/response patterns, pagination, versioning, error handling, and the trade-offs that separate a good API from a bad one.
241 views
3
What is REST?
REST (Representational State Transfer) is an architectural style for designing networked applications, introduced by Roy Fielding in his 2000 doctoral dissertation. It is not a protocol or standard - it is a set of constraints that, when followed, produce systems that are scalable, simple, and maintainable.
The Six REST Constraints
Client-Server: Separate the user interface (client) from data storage and business logic (server). This allows each to evolve independently.
Stateless: Each request from client to server must contain all the information needed to process it. The server does not store session state between requests. Authentication tokens, for example, are sent with every request.
Cacheable: Responses must indicate whether they can be cached. Proper caching eliminates redundant interactions and improves performance.
Uniform Interface: All resources are accessed through a consistent, standardized interface (URLs + HTTP methods). This simplifies the architecture and enables independent evolution.
Layered System: The client cannot tell whether it is connected directly to the server or to an intermediary (load balancer, CDN, API gateway). Each layer only knows about the layer it interacts with.
Code on Demand (optional): Servers can extend client functionality by sending executable code (e.g., JavaScript). This is the only optional constraint.
REST vs RESTful
- REST is the architectural style.
- RESTful describes an API that follows REST constraints.
- In practice, most APIs marketed as "REST" are actually "REST-ish" - they use HTTP and JSON but may violate some constraints (e.g., storing session state on the server).
Resources: The Core Abstraction
In REST, everything is a resource - a user, a post, an order, a payment. Each resource has:
- A unique identifier (URL)
- Representations (JSON, XML, etc.)
- Standard operations (HTTP methods)
URL Design & Resource Naming
URL design is the most visible part of an API. Well-designed URLs are intuitive, predictable, and self-documenting.
Core Principles
Use nouns, not verbs: URLs represent resources (things), not actions.
- Good:
GET /users/42 - Bad:
GET /getUser?id=42
Use plural nouns: Collections should be plural for consistency.
- Good:
/users,/orders,/products - Bad:
/user,/order,/product
Use hierarchy for relationships: Nest related resources.
GET /users/42/orders- all orders for user 42GET /users/42/orders/7- order 7 for user 42- Limit nesting to 2 levels. Deeper nesting becomes unwieldy:
/users/42/orders/7/items/3/reviewsis too deep.
Use kebab-case: Separate words with hyphens.
- Good:
/order-items,/user-profiles - Bad:
/orderItems,/order_items
No trailing slashes: Be consistent. Pick one convention and stick to it.
- Preferred:
/users/42(no trailing slash)
URL Design Examples
# Collection operations
GET /api/v1/users - List all users
POST /api/v1/users - Create a new user
# Single resource operations
GET /api/v1/users/42 - Get user 42
PUT /api/v1/users/42 - Replace user 42
PATCH /api/v1/users/42 - Partially update user 42
DELETE /api/v1/users/42 - Delete user 42
# Nested resources
GET /api/v1/users/42/posts - List user 42's posts
POST /api/v1/users/42/posts - Create a post for user 42
GET /api/v1/users/42/posts/101 - Get post 101 by user 42
# Filtering, sorting, pagination via query parameters
GET /api/v1/users?role=admin&sort=created_at&order=desc&page=2&limit=20
# Search
GET /api/v1/users/search?q=john
# Actions that don't map to CRUD (use sparingly)
POST /api/v1/orders/42/cancel
POST /api/v1/users/42/reset-passwordWhat About Actions That Are Not CRUD?
Some operations do not fit neatly into create/read/update/delete:
- Use POST with a descriptive sub-resource:
POST /orders/42/cancel - Model the action as a resource: Instead of
POST /users/42/activate, create an activation resource:POST /users/42/activations - Do not use GET for side effects:
GET /users/42/deleteis a REST anti-pattern. GET must be safe (no side effects).
HTTP Methods & Status Codes in Practice
Mapping CRUD to HTTP Methods
| Operation | HTTP Method | URL Pattern | Request Body | Response |
|---|---|---|---|---|
| Create | POST | /users | {"name": "Alice"} | 201 Created + new resource |
| Read (list) | GET | /users | None | 200 OK + array |
| Read (single) | GET | /users/42 | None | 200 OK + object |
| Replace | PUT | /users/42 | Full object | 200 OK + updated resource |
| Partial update | PATCH | /users/42 | Partial fields | 200 OK + updated resource |
| Delete | DELETE | /users/42 | None | 204 No Content |
Choosing the Right Status Code
| Situation | Status Code | Meaning |
|---|---|---|
| Successful GET | 200 OK | Resource found and returned |
| Successful POST (created) | 201 Created | New resource created (include Location header) |
| Successful DELETE | 204 No Content | Deleted successfully, no body |
| Accepted for async processing | 202 Accepted | Request received, processing later |
| Bad request body | 400 Bad Request | Malformed JSON, missing required fields |
| Missing or invalid auth | 401 Unauthorized | Authentication required or failed |
| Insufficient permissions | 403 Forbidden | Authenticated but not authorized |
| Resource not found | 404 Not Found | The resource does not exist |
| Method not allowed | 405 Method Not Allowed | e.g., PUT on a read-only resource |
| Conflict | 409 Conflict | e.g., duplicate email during registration |
| Validation failure | 422 Unprocessable Entity | Request is well-formed but semantically invalid |
| Rate limited | 429 Too Many Requests | Client exceeded rate limit |
| Server error | 500 Internal Server Error | Unexpected server-side failure |
| Service unavailable | 503 Service Unavailable | Server overloaded or under maintenance |
401 vs 403: A Common Confusion
- 401 Unauthorized: "I don't know who you are. Please authenticate." (Missing or invalid token)
- 403 Forbidden: "I know who you are, but you are not allowed to do this." (Valid token, insufficient permissions)
Despite the confusing name, 401 means unauthenticated, and 403 means unauthorized.
Pagination, Filtering & Sorting
Any endpoint that returns a list of resources must support pagination. Returning 10 million users in a single response is a guaranteed way to crash your server and your client.
Pagination Strategies
1. Offset-Based Pagination
GET /api/v1/users?page=3&limit=20How it works: Skip (page - 1) * limit rows, return the next limit rows.
Pros: Simple, allows jumping to any page.
Cons: Performance degrades on large datasets (SQL's OFFSET scans skipped rows). Inconsistent results if data changes between pages (items shift).
2. Cursor-Based Pagination
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20How it works: The cursor is an opaque token (often a Base64-encoded ID or timestamp) pointing to the last item of the previous page. The server fetches items after the cursor. Pros: Consistent performance regardless of page depth. Stable results even when data changes. Cons: Cannot jump to a specific page. More complex to implement.
3. Keyset Pagination
GET /api/v1/users?after_id=100&limit=20
-- SQL: SELECT * FROM users WHERE id > 100 ORDER BY id LIMIT 20How it works: Similar to cursor-based, but the cursor is a visible field value (like an ID or timestamp) rather than an opaque token. Pros: Efficient (uses index), easy to understand. Cons: Requires a unique, sortable field.
Pagination Response Format
{
"data": [...],
"pagination": {
"total": 1542,
"limit": 20,
"offset": 40,
"hasMore": true,
"nextCursor": "eyJpZCI6MTYwfQ"
}
}Which Pagination Strategy to Choose?
| Use Case | Strategy | Why |
|---|---|---|
| Admin dashboard with page numbers | Offset | Users expect to jump to page 5 |
| Infinite scroll (social feed) | Cursor | No need for page numbers, data changes frequently |
| Large datasets (millions of rows) | Cursor/Keyset | Offset becomes slow at high page numbers |
| Public API | Cursor | More stable contract, doesn't expose dataset size |
Filtering and Sorting
# Filtering
GET /api/v1/products?category=electronics&price_min=100&price_max=500&in_stock=true
# Sorting
GET /api/v1/products?sort=price&order=asc
GET /api/v1/products?sort=-created_at (prefix with - for descending)
# Combined
GET /api/v1/products?category=electronics&sort=-price&limit=20&cursor=abc123Best practice: Always define a default sort order (usually by creation date, descending). Never return unordered results - it makes pagination unpredictable.
API Versioning
APIs evolve. You will add new fields, change behavior, and occasionally make breaking changes. Versioning lets you evolve without breaking existing clients.
Versioning Strategies
1. URL Path Versioning (Most Common)
GET /api/v1/users
GET /api/v2/usersPros: Explicit, easy to understand, easy to route at the API gateway/load balancer level. Cons: Duplicates routes. Clients must update URLs to adopt new versions. Used by: Stripe, Twilio, GitHub (partially).
2. Query Parameter Versioning
GET /api/users?version=2Pros: Same URL, easy to default to latest version. Cons: Less visible, can be forgotten. Harder to cache (cache keys must include the parameter).
3. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+jsonPros: Clean URLs, follows HTTP content negotiation semantics. Cons: Hidden from URL inspection, harder to test in a browser, less discoverable. Used by: GitHub API.
4. Date-Based Versioning
GET /api/users
Stripe-Version: 2024-01-15Pros: Fine-grained, allows gradual rollout of changes tied to specific dates. Cons: Complex to manage internally, requires mapping dates to behavior. Used by: Stripe.
Best Practice: URL Path Versioning
For most APIs, URL path versioning (/v1/, /v2/) is the recommended approach because:
- It is immediately visible in every request
- API gateways can route by path prefix
- It is the most widely understood convention
- Caching works naturally (different URLs = different cache entries)
When to Increment the Version?
Breaking changes require a new version:
- Removing a field from the response
- Changing the type of a field
- Changing the meaning of a status code
- Requiring new mandatory parameters
Non-breaking changes do not require a new version:
- Adding a new optional field to the response
- Adding a new optional query parameter
- Adding a new endpoint
- Adding a new status code for a new scenario
Error Handling & Response Design
Consistent error handling is what separates a professional API from an amateur one. Clients need to programmatically handle errors, so the format must be predictable.
Standard Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body is invalid.",
"details": [
{
"field": "email",
"issue": "Must be a valid email address.",
"value": "not-an-email"
},
{
"field": "age",
"issue": "Must be a positive integer.",
"value": -5
}
],
"requestId": "req_abc123",
"documentation": "https://api.example.com/docs/errors#VALIDATION_ERROR"
}
}Error Response Best Practices
Use the HTTP status code correctly: The status code (400, 404, 500) is the primary error signal. The body provides details.
Include a machine-readable error code:
VALIDATION_ERROR,RESOURCE_NOT_FOUND,RATE_LIMIT_EXCEEDED. Clients can switch on this code without parsing human-readable messages.Include a human-readable message: For developer debugging, not for end-user display.
Include a request ID: Essential for debugging. The client can report this ID, and you can trace it through your logs.
Include field-level details for validation errors: Clients need to know which fields failed and why.
Never expose internal details in production: Stack traces, SQL queries, internal server names - these are security risks.
Successful Response Design
// Single resource
{
"data": {
"id": 42,
"name": "Alice",
"email": "[email protected]",
"createdAt": "2024-01-15T10:30:00Z"
}
}
// Collection
{
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
],
"pagination": {
"total": 150,
"limit": 20,
"offset": 0,
"hasMore": true
}
}Wrapping Responses
Always wrap your responses in a top-level object ({ "data": ... }) rather than returning a raw array. This allows you to add metadata (pagination, request ID, deprecation warnings) without breaking the response structure.
Trade-offs & When REST Falls Short
REST is the default choice for most APIs, but it has limitations that other styles address.
Over-fetching and Under-fetching
Over-fetching: GET /users/42 returns all 30 fields when the client only needs name and avatar. Wasted bandwidth, especially on mobile.
Under-fetching: To show a user's profile with their posts and followers, the client needs three separate requests:
GET /users/42GET /users/42/postsGET /users/42/followers
GraphQL solves this by letting the client specify exactly which fields and nested resources it needs in a single query.
REST vs GraphQL vs gRPC
| Aspect | REST | GraphQL | gRPC |
|---|---|---|---|
| Protocol | HTTP (any version) | HTTP (typically POST) | HTTP/2 |
| Data format | JSON (typically) | JSON | Protocol Buffers (binary) |
| Schema | OpenAPI/Swagger (optional) | Required (SDL) | Required (.proto files) |
| Fetching | Fixed response shapes | Client specifies exactly what it needs | Fixed response shapes |
| Performance | Good | Good (fewer requests) | Excellent (binary, multiplexed) |
| Caching | Excellent (HTTP caching built-in) | Difficult (all requests are POST) | Difficult (binary, no HTTP caching) |
| Browser support | Native | Native (over HTTP) | Limited (requires grpc-web proxy) |
| Learning curve | Low | Medium | High |
| Best for | Public APIs, CRUD applications | Mobile apps, complex UIs with varied data needs | Internal microservice communication |
When to Choose What
- REST: Default choice for public-facing APIs, CRUD-heavy applications, and when HTTP caching is important.
- GraphQL: When clients have diverse data needs (mobile vs web), when over-fetching is a real problem, or when you want a single endpoint for multiple resources.
- gRPC: For internal service-to-service communication where latency matters, when you want strong typing and code generation, or for streaming use cases.
REST API Design Anti-Patterns
- Verbs in URLs:
/getUsers,/createOrder- use HTTP methods instead. - Ignoring status codes: Returning 200 with
{"error": true}in the body. - Inconsistent naming: Mixing
/userProfileswith/order-items. - No pagination on list endpoints: Returning unbounded results.
- Exposing database IDs as sequential integers: Security risk (enumeration attacks). Consider UUIDs or opaque IDs.
- Tight coupling to database schema: API fields should not mirror database columns. The API is a contract; the database is an implementation detail.
REST API Design in Interviews
How to Design APIs in a System Design Interview
When the interviewer says "let's define the API," follow this structure:
Step 1: Identify the Resources
List the main entities in the system.
- URL shortener:
urls,analytics - Instagram:
users,posts,comments,likes,follows - Chat system:
conversations,messages,users
Step 2: Define CRUD Operations
For each resource, specify the key endpoints:
POST /api/v1/urls - Create a short URL
GET /api/v1/urls/:id - Get URL details
GET /:shortCode - Redirect (special case)
DELETE /api/v1/urls/:id - Delete a short URL
GET /api/v1/urls/:id/analytics - Get click analyticsStep 3: Specify Request/Response Shapes
// POST /api/v1/urls
// Request
{ "longUrl": "https://example.com/very/long/path", "customAlias": "my-link" }
// Response (201 Created)
{ "data": { "id": "abc123", "shortUrl": "https://short.ly/my-link", "longUrl": "...", "createdAt": "..." } }Step 4: Call Out Special Considerations
- Pagination strategy for list endpoints
- Authentication method (JWT, API key)
- Rate limiting policy
- Idempotency keys for non-idempotent operations (POST)
Quick Reference: Commonly Designed APIs
| System | Key Endpoints |
|---|---|
| URL Shortener | POST /urls, GET /:code (redirect), GET /urls/:id/stats |
POST /posts (with photo upload), GET /feed, POST /users/:id/follow | |
| Chat | POST /conversations, POST /conversations/:id/messages, GET /conversations/:id/messages?cursor=X |
| Rate Limiter | GET /rate-limit/check?client_id=X, POST /rate-limit/increment |
POST /tweets, GET /timeline, GET /users/:id/tweets?cursor=X |
Real-World Examples
How real systems implement this in production
Stripe is widely considered the gold standard of REST API design. It uses consistent resource naming (/v1/customers, /v1/charges), predictable behavior across endpoints, detailed error responses with machine-readable codes, and date-based versioning (Stripe-Version header) for backward compatibility.
Trade-off: Stripe's API is excellent for consistency but requires clients to manage versioning headers and handle webhook events for async operations like payment processing.
GitHub's REST API provides extensive use of hypermedia links (HATEOAS), cursor-based pagination with Link headers, and conditional requests (ETag/If-None-Match) for efficient caching. They also offer a GraphQL API (v4) alongside REST (v3) for flexible querying.
Trade-off: Offering both REST and GraphQL APIs doubles the maintenance burden but serves different client needs: REST for simple integrations, GraphQL for complex data fetching in their web UI.
Twitter's v2 API uses field selection (fields parameter) to reduce over-fetching, expansions to include related objects in a single request, and cursor-based pagination with pagination tokens. Rate limits are communicated via headers (X-Rate-Limit-Remaining).
Trade-off: Twitter's field selection and expansions add complexity for API consumers but significantly reduce bandwidth usage and the number of requests needed, especially important for their high-traffic platform.
Quick Interview Phrases
Key terms to use in your answer
Common Interview Questions
Questions you might be asked about this topic
GET /feed with cursor-based pagination, filter params. Separate endpoints for posts, comments, likes. Use ETags for caching. Rate limit by user. Discuss fan-out on read vs write.
Offset is simple but breaks with insertions/deletions and degrades at deep pages. Cursor (keyset) is stable and performant. Use offset for admin dashboards, cursor for user-facing feeds.
URL path (/v1/), header (Accept-Version), or query param. Path is most common and explicit. Never break existing clients. Use deprecation warnings. Stripe's approach is a good reference.
GET, PUT, DELETE are idempotent - repeating them produces the same result. POST is not. Idempotency is critical for retry safety in distributed systems. Use idempotency keys for POST.
Interview Tips
How to discuss this topic effectively
When defining APIs in an interview, start by listing the resources (nouns) in the system, then map CRUD operations to HTTP methods. This shows structured thinking.
Always mention pagination for list endpoints. Say 'This returns a paginated list using cursor-based pagination' rather than just 'This returns the list of users.' Interviewers notice this attention to detail.
If you are designing an API that accepts user input, mention input validation and the error response format. This signals that you think about edge cases and developer experience.
Know when to suggest alternatives to REST. If the interviewer's system has diverse client needs (mobile, web, internal tools), mention GraphQL. If it involves microservice-to-microservice communication, mention gRPC.
Include a versioning strategy in your API design. Saying 'All endpoints are prefixed with /api/v1/ so we can introduce breaking changes in /api/v2/ without affecting existing clients' takes 5 seconds and shows foresight.
Mention idempotency keys for POST endpoints in payment or financial systems. 'The client sends an Idempotency-Key header so that retrying a failed payment creation does not double-charge the user.' This is a senior-level concern.
Common Mistakes
Pitfalls to avoid in interviews
Using verbs in URLs instead of nouns
REST URLs should represent resources (nouns), not actions (verbs). Use HTTP methods to indicate the action. Instead of GET /getUsers or POST /createUser, use GET /users and POST /users.
Returning 200 OK for error responses with error details in the body
Use proper HTTP status codes (400, 401, 403, 404, 500) so that clients, proxies, and monitoring tools can correctly interpret the response. The body should provide additional detail, but the status code is the primary signal.
Returning a raw array as the top-level response
Always wrap responses in an object: { "data": [...] }. This allows you to add metadata (pagination, warnings, request ID) later without breaking the response contract. A raw array cannot be extended.
Not implementing pagination on list endpoints
Every endpoint that returns a collection must be paginated. Without pagination, a single request could return millions of records, overwhelming both the server and the client. Default to a reasonable page size (20-100) with a maximum limit.
