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.

System Design
/

REST API Design & Best Practices

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.

System Design
Easy
rest
api-design
http-methods
pagination
versioning
error-handling
crud
beginner

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

  1. Client-Server: Separate the user interface (client) from data storage and business logic (server). This allows each to evolve independently.

  2. 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.

  3. Cacheable: Responses must indicate whether they can be cached. Proper caching eliminates redundant interactions and improves performance.

  4. Uniform Interface: All resources are accessed through a consistent, standardized interface (URLs + HTTP methods). This simplifies the architecture and enables independent evolution.

  5. 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.

  6. 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

  1. Use nouns, not verbs: URLs represent resources (things), not actions.

    • Good: GET /users/42
    • Bad: GET /getUser?id=42
  2. Use plural nouns: Collections should be plural for consistency.

    • Good: /users, /orders, /products
    • Bad: /user, /order, /product
  3. Use hierarchy for relationships: Nest related resources.

    • GET /users/42/orders - all orders for user 42
    • GET /users/42/orders/7 - order 7 for user 42
    • Limit nesting to 2 levels. Deeper nesting becomes unwieldy: /users/42/orders/7/items/3/reviews is too deep.
  4. Use kebab-case: Separate words with hyphens.

    • Good: /order-items, /user-profiles
    • Bad: /orderItems, /order_items
  5. No trailing slashes: Be consistent. Pick one convention and stick to it.

    • Preferred: /users/42 (no trailing slash)

URL Design Examples

Text
# 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-password

What 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/delete is a REST anti-pattern. GET must be safe (no side effects).

HTTP Methods & Status Codes in Practice

Mapping CRUD to HTTP Methods

OperationHTTP MethodURL PatternRequest BodyResponse
CreatePOST/users{"name": "Alice"}201 Created + new resource
Read (list)GET/usersNone200 OK + array
Read (single)GET/users/42None200 OK + object
ReplacePUT/users/42Full object200 OK + updated resource
Partial updatePATCH/users/42Partial fields200 OK + updated resource
DeleteDELETE/users/42None204 No Content

Choosing the Right Status Code

SituationStatus CodeMeaning
Successful GET200 OKResource found and returned
Successful POST (created)201 CreatedNew resource created (include Location header)
Successful DELETE204 No ContentDeleted successfully, no body
Accepted for async processing202 AcceptedRequest received, processing later
Bad request body400 Bad RequestMalformed JSON, missing required fields
Missing or invalid auth401 UnauthorizedAuthentication required or failed
Insufficient permissions403 ForbiddenAuthenticated but not authorized
Resource not found404 Not FoundThe resource does not exist
Method not allowed405 Method Not Allowede.g., PUT on a read-only resource
Conflict409 Conflicte.g., duplicate email during registration
Validation failure422 Unprocessable EntityRequest is well-formed but semantically invalid
Rate limited429 Too Many RequestsClient exceeded rate limit
Server error500 Internal Server ErrorUnexpected server-side failure
Service unavailable503 Service UnavailableServer 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
Text
GET /api/v1/users?page=3&limit=20

How 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
Text
GET /api/v1/users?cursor=eyJpZCI6MTAwfQ&limit=20

How 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
Text
GET /api/v1/users?after_id=100&limit=20
-- SQL: SELECT * FROM users WHERE id > 100 ORDER BY id LIMIT 20

How 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

JSON
{
    "data": [...],
    "pagination": {
        "total": 1542,
        "limit": 20,
        "offset": 40,
        "hasMore": true,
        "nextCursor": "eyJpZCI6MTYwfQ"
    }
}

Which Pagination Strategy to Choose?

Use CaseStrategyWhy
Admin dashboard with page numbersOffsetUsers expect to jump to page 5
Infinite scroll (social feed)CursorNo need for page numbers, data changes frequently
Large datasets (millions of rows)Cursor/KeysetOffset becomes slow at high page numbers
Public APICursorMore stable contract, doesn't expose dataset size

Filtering and Sorting

Text
# 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=abc123

Best 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)
Text
GET /api/v1/users
GET /api/v2/users

Pros: 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
Text
GET /api/users?version=2

Pros: 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
Text
GET /api/users
Accept: application/vnd.myapi.v2+json

Pros: 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
Text
GET /api/users
Stripe-Version: 2024-01-15

Pros: 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

JSON
{
    "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

  1. Use the HTTP status code correctly: The status code (400, 404, 500) is the primary error signal. The body provides details.

  2. 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.

  3. Include a human-readable message: For developer debugging, not for end-user display.

  4. Include a request ID: Essential for debugging. The client can report this ID, and you can trace it through your logs.

  5. Include field-level details for validation errors: Clients need to know which fields failed and why.

  6. Never expose internal details in production: Stack traces, SQL queries, internal server names - these are security risks.

Successful Response Design

JSON
// 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:

  1. GET /users/42
  2. GET /users/42/posts
  3. GET /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

AspectRESTGraphQLgRPC
ProtocolHTTP (any version)HTTP (typically POST)HTTP/2
Data formatJSON (typically)JSONProtocol Buffers (binary)
SchemaOpenAPI/Swagger (optional)Required (SDL)Required (.proto files)
FetchingFixed response shapesClient specifies exactly what it needsFixed response shapes
PerformanceGoodGood (fewer requests)Excellent (binary, multiplexed)
CachingExcellent (HTTP caching built-in)Difficult (all requests are POST)Difficult (binary, no HTTP caching)
Browser supportNativeNative (over HTTP)Limited (requires grpc-web proxy)
Learning curveLowMediumHigh
Best forPublic APIs, CRUD applicationsMobile apps, complex UIs with varied data needsInternal 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

  1. Verbs in URLs: /getUsers, /createOrder - use HTTP methods instead.
  2. Ignoring status codes: Returning 200 with {"error": true} in the body.
  3. Inconsistent naming: Mixing /userProfiles with /order-items.
  4. No pagination on list endpoints: Returning unbounded results.
  5. Exposing database IDs as sequential integers: Security risk (enumeration attacks). Consider UUIDs or opaque IDs.
  6. 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:

Text
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 analytics
Step 3: Specify Request/Response Shapes
JSON
// 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

SystemKey Endpoints
URL ShortenerPOST /urls, GET /:code (redirect), GET /urls/:id/stats
InstagramPOST /posts (with photo upload), GET /feed, POST /users/:id/follow
ChatPOST /conversations, POST /conversations/:id/messages, GET /conversations/:id/messages?cursor=X
Rate LimiterGET /rate-limit/check?client_id=X, POST /rate-limit/increment
TwitterPOST /tweets, GET /timeline, GET /users/:id/tweets?cursor=X

Real-World Examples

How real systems implement this in production

Stripe API

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 REST API

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/X API v2

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

resource-oriented design
idempotent operations
cursor-based pagination
HATEOAS
API versioning strategy
rate limiting

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.

Interview Tips

How to discuss this topic effectively

1

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.

2

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.

3

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.

4

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.

5

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.

6

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.