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.
2. The 6 Constraints of REST
To be truly RESTful, a system must adhere to these six constraints:
- Client-Server: Separation of concerns. The client handles the UI, the server handles the data.
- Stateless: The server stores no session state between requests. Every request must contain all necessary information (e.g., Auth Token).
- Cacheable: Responses must define themselves as cacheable or not (e.g.,
Cache-Controlheader). - Uniform Interface: Resources are identified by URLs (URIs), and manipulation is done via standard HTTP verbs.
- Layered System: The client doesn’t know if it’s connected directly to the server or an intermediary (like a Load Balancer or CDN).
- 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: 123in RAM → Client sendsSessionID: 123. - Problem: If Server A crashes, the user is logged out. Scaling requires “Sticky Sessions”.
Stateless (Tokens):
- Client logs in → Server issues a
JWT(containsuser_id: 1) → Client sendsJWTon 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:
Auth: Bearer xyz
Miss?
(Client unaware)
user_id = "1"
FROM users
WHERE id=1
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:
- Client generates a unique UUID (e.g.,
8a3f-12b9) when the user clicks “Pay”. - Client sends
POST /chargeswithIdempotency-Key: 8a3f-12b9. - 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.
- Miss (First Attempt): Process the charge. Save the result (Response Body + Status Code) in DB linked to the key. Return
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)
Server (Stripe)
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.
8.2 Cursor Pagination (Recommended for Feeds)
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.
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.
- Why? Mobile devices have limited battery and bandwidth. Fetching data from 5 different REST endpoints (
- 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.