REST API Design Strategies

[!TIP] Interview Tip: In a System Design interview, never just say “We’ll use a REST API.” Explain why (statelessness, cacheability) and how you handle complex scenarios like pagination, versioning, and idempotency.

REST (Representational State Transfer) is not a protocol, but an architectural style for distributed hypermedia systems, defined by Roy Fielding in his 2000 PhD dissertation.

While everyone claims to build “RESTful” APIs, most are actually just “HTTP APIs”. To truly understand REST, we must look at the Richardson Maturity Model.


1. The Richardson Maturity Model

This model breaks down the principal elements of a REST approach into three steps:

The Swamp of POX

Uses HTTP as a transport protocol (usually just POST). RPC style XML/JSON.

POST /apiService Body: <xml>getUser</xml>

2. The 6 Constraints of REST

To be truly RESTful, a system must adhere to these six constraints:

  1. Client-Server: Separation of concerns. The client handles the UI, the server handles the data.
  2. Stateless: The server stores no session state between requests. Every request must contain all necessary information (e.g., Auth Token).
  3. Cacheable: Responses must define themselves as cacheable or not (e.g., Cache-Control header).
  4. Uniform Interface: Resources are identified by URLs (URIs), and manipulation is done via standard HTTP verbs.
  5. Layered System: The client doesn’t know if it’s connected directly to the server or an intermediary (like a Load Balancer or CDN).
  6. Code on Demand (Optional): The server can extend client functionality by sending code (e.g., JavaScript).

Deep Dive: Statelessness vs Stateful

This is often misunderstood. “Stateless” does not mean the application has no state (it has a database!). It means the Server Application Layer keeps no memory of past requests.

Stateful (Session Store):

  • Client logs in → Server creates SessionID: 123 in RAM → Client sends SessionID: 123.
  • Problem: If Server A crashes, the user is logged out. Scaling requires “Sticky Sessions”.

Stateless (Tokens):

  • Client logs in → Server issues a JWT (contains user_id: 1) → Client sends JWT on every request.
  • Benefit: Any server can handle the request. Horizontal scaling is trivial.

REST Request Lifecycle Diagram

The diagram below shows how a RESTful API request flows through a system, demonstrating the 6 constraints:

RESTful API Request Lifecycle
Demonstrating Statelessness, Cacheability, and Layered Architecture
Client
GET /users/1
Auth: Bearer xyz
CDN / Cache
✓ Cacheable
Cache-Control: max-age=300
Cache
Miss?
Load Balancer
Layered System
(Client unaware)
API Server
✓ Stateless
No session stored
Extract JWT
user_id = "1"
SELECT *
FROM users
WHERE id=1
Database
200 OK + Cache-Control Header
Client-Server Separation
Cacheable Responses
Stateless (JWT contains all context)

3. HTTP Methods: Safe vs. Idempotent

Understanding these two properties is crucial for building reliable distributed systems.

  • Safe: The method does not change the state of the resource (Read-only).
  • Idempotent: Making the same request multiple times has the same effect as making it once.
Method Usage Safe? Idempotent?
GET Retrieve a resource. ✅ Yes ✅ Yes
POST Create a new resource. ❌ No ❌ No
PUT Update/Replace a resource. ❌ No ✅ Yes
PATCH Partial update. ❌ No ❌ No (usually)
DELETE Delete a resource. ❌ No ✅ Yes

4. Case Study: Idempotency at Stripe

In a perfect world, networks never fail. In reality, network timeouts happen all the time. Imagine a customer clicks “Pay $50”. The request reaches Stripe, Stripe charges the card, but the response is lost on the way back due to a bad connection. The customer sees a spinner, gets frustrated, and clicks “Pay” again.

Result: Double Charge. 😱

The Solution: Idempotency Keys

Stripe (and most modern payment APIs) solves this with a header: Idempotency-Key.

The Flow:

  1. Client generates a unique UUID (e.g., 8a3f-12b9) when the user clicks “Pay”.
  2. Client sends POST /charges with Idempotency-Key: 8a3f-12b9.
  3. Server checks its database (Redis/SQL) for key 8a3f-12b9.
    • Miss (First Attempt): Process the charge. Save the result (Response Body + Status Code) in DB linked to the key. Return 200 OK.
    • Hit (Retry): The server sees the key exists. It skips the charge logic and returns the saved response immediately.

Interactive Visualizer: Idempotency Simulator

Use this tool to simulate a Payment System. Adjust the Network Lag to see how timeouts affect the user experience. Observe the Network Log to see the actual headers being sent.

Client (User App)

Current Action:
Idle

Server (Stripe)

Wallet Balance:
$1000
Network Inspector
> Ready to capture requests...

5. API Paradigms Comparison

Which style should you choose for your system?

Feature REST GraphQL gRPC
Protocol HTTP/1.1 (Text) HTTP/1.1 (Text) HTTP/2 (Binary)
Data Fetching Multiple endpoints (Over-fetching) Single endpoint (Exact fetching) RPC methods (Strongly typed)
Performance Good Moderate (JSON parsing overhead) Excellent (Protobuf is compact)
Browser Support Universal Universal Requires Proxy (gRPC-Web)
Best For Public APIs, CRUD Complex Client Data Requirements Internal Microservices
Schema OpenAPI (Swagger) GraphQL Schema (SDL) Protocol Buffers (.proto)

[!TIP] Why gRPC for Internal Services? REST sends JSON, which is text-based and bulky. gRPC sends Protocol Buffers, which are binary and 10x smaller/faster. For internal traffic (East-West), this bandwidth saving is massive.


6. HATEOAS: The Engine of State

Hypermedia As The Engine Of Application State. This is what distinguishes a “Level 3” REST API. It means the API response contains links to valid next actions, allowing the client to navigate the API dynamically without hardcoding URLs.

Why HATEOAS?

It decouples the Client from the Server’s URL structure. If the server changes /users/1/deposit to /users/1/add-money, the client doesn’t break—it simply follows the deposit link provided in the previous response.

With HATEOAS (HAL Format):

{
  "id": 1,
  "balance": 100,
  "_links": {
    "self": { "href": "/accounts/1" },
    "deposit": { "href": "/accounts/1/deposit" },
    "withdraw": { "href": "/accounts/1/withdraw" }
  }
}

7. Content Negotiation

A truly RESTful API separates the Resource from its Representation. The client specifies what it wants using the Accept header.

Request:

GET /users/1 HTTP/1.1
Accept: application/xml

Response:

<user>
  <id>1</id>
  <name>Alice</name>
</user>

8. Pagination Strategies

When an API returns a list of items, you must paginate to protect your database.

8.1 Offset Pagination (Standard)

GET /users?page=3&limit=10

  • SQL: SELECT * FROM users LIMIT 10 OFFSET 20;
  • Pros: Simple, easy to jump to specific page.
  • Cons: Slow for deep pages. The DB must read 20 rows to skip them (O(N)). Not consistent if rows are inserted/deleted during browsing.

GET /users?cursor=user_id_1024&limit=10

  • SQL: SELECT * FROM users WHERE id > 1024 LIMIT 10;
  • Pros: Fast (O(1)) if indexed. Consistent even if new items are added.
  • Cons: Can’t jump to “Page 50”. Requires a unique, sortable field (like a Snowflake ID).

9. HTTP Status Codes Cheat Sheet

Don’t just return 200 OK for everything!

2xx: Success

  • 200 OK: Generic success.
  • 201 Created: Resource created successfully (use with POST).
  • 204 No Content: Action successful, but response body is empty (use with DELETE).

3xx: Redirection

  • 301 Moved Permanently: The resource has a new permanent URL. (Browser caches this redirect).
  • 302 Found: Temporary redirect.
  • 304 Not Modified: Resource hasn’t changed (Cache hit).

4xx: Client Errors

  • 400 Bad Request: Invalid JSON, missing fields.
  • 401 Unauthorized: Authentication failed (Missing or invalid token).
  • 403 Forbidden: Authenticated, but not authorized (Permissions).
  • 404 Not Found: Resource doesn’t exist.
  • 429 Too Many Requests: Rate Limit exceeded.

5xx: Server Errors

  • 500 Internal Server Error: Unhandled exception in code.
  • 502 Bad Gateway: Upstream service (e.g., Database) failed.
  • 503 Service Unavailable: Server is overloaded or down for maintenance.

10. Versioning Strategies

APIs evolve. How do you handle breaking changes?

Interactive Strategy Switcher

Select a strategy to see how the Request and Response structures change.

HTTP Request
Analysis

11. When NOT to use REST

REST is the default for most public APIs, but it’s not always the best choice.

  • Internal Microservices: Use gRPC.
    • Why? REST (JSON) is text-based and slow to parse. gRPC (Protobuf) is binary, strongly typed, and supports streaming. It’s 10x faster for internal traffic.
  • Mobile Apps / Complex Frontends: Use GraphQL.
    • Why? Mobile devices have limited battery and bandwidth. Fetching data from 5 different REST endpoints (/user, /posts, /comments) wastes resources. GraphQL fetches everything in one go.
  • Real-Time Data: Use WebSockets or SSE.
    • Why? REST is request-response. It can’t “push” data when a stock price changes.

12. Summary

  • Level 2 REST (Verbs + Status Codes) is sufficient for most apps.
  • Use Idempotency Keys for sensitive operations (Payments) to handle retries safely.
  • Cursor Pagination beats Offset Pagination for large datasets.
  • Use standard HTTP Status Codes to communicate errors effectively.

Next, let’s look at how to solve the “Over-fetching” problem with GraphQL Basics.