RESTful API Design
Resources are nouns. Pagination strategies. Response shape standards. HATEOAS.
Where REST Came From
Before there was REST, there was the World Wide Web — and that's not a coincidence.
In 1989, Tim Berners-Lee at CERN built the Web on three technologies:
• URL — a unique address for any resource
• HTTP — a way to retrieve resources by URL
• HTML — a format for those resources
It was originally meant for sharing physics papers within CERN. When CERN released it publicly in 1993, the scale exploded and the cracks showed. Every hyperlink click opened a brand new TCP connection. State was a mess. Caching was haphazard.
In 2000, Roy Fielding (one of the authors of the HTTP/1.1 specification) wrote his PhD thesis on the architectural constraints that made the Web work — and called the style "REST" (Representational State Transfer). It wasn't a new protocol. It was a description of what the Web was already doing well, formalized so other systems could be built the same way.
What was REST competing against?
SOAP (Simple Object Access Protocol) — popular in the early 2000s. SOAP wraps every remote call in XML, sends it over HTTP, and defines strict contracts using yet another XML format (WSDL). It was rigid, verbose, and notably: SOAP only used POST. It treated HTTP as a dumb tunnel, ignoring caching, status codes, and method semantics.
REST won because it embraced HTTP rather than tunneling through it. Caching, status codes, methods — REST uses them all. APIs became simpler, more cacheable, and language-agnostic in a way SOAP never managed.
That's the inheritance: every REST API you build today is a direct descendant of how Tim Berners-Lee designed the Web. The constraints aren't arbitrary — they're what made the Web scale to billions of users.
REST Principles
REST is an architectural style, not a protocol. Six constraints define it:
- Client-Server — Separation of concerns. UI from data storage.
- Stateless — Each request contains all info needed. No session state on server.
- Cacheable — Responses declare if they're cacheable.
- Uniform Interface — Resources identified by URIs, manipulated via representations.
- Layered System — Client can't tell if it's talking to the real server or a proxy.
- Code on Demand (optional) — Server can send executable code (JS) to clients.
Most "REST APIs" only follow 1-4. That's fine. True REST (HATEOAS) is rarely implemented.
Three powerful ideas hide inside the Uniform Interface constraint:
• Everything addressable has a URI. /users/42 isn't just data — it's the unique address of that resource.
• Never transfer the resource itself, only a representation. The same /users/42 resource could be returned as JSON, XML, or HTML depending on the Accept header.
• Responses must be self-describing. The server includes enough metadata (Content-Type, status, headers) for the client to know how to process the response, without needing out-of-band documentation.
Idempotency — The Quietly Important Property
An operation is idempotent if calling it multiple times has the same effect as calling it once.
GET /users/42 — idempotent. Reading doesn't change anything.
PUT /users/42 — idempotent. Replacing with the same data leaves the same result.
DELETE /users/42 — idempotent. After the first call, subsequent calls do nothing new.
POST /users — NOT idempotent. Each call creates a new user.
Why this matters in practice:
Network failures. If a request times out, can the client safely retry? For idempotent methods, yes. For POST, no — without extra machinery, you might create duplicates. (See: idempotency keys, used by Stripe, AWS, and others.)
Caching. Only safe (idempotent and side-effect-free) responses can be cached. That's why GET responses are routinely cached and POST responses aren't.
Designing for failure. When you're choosing between PUT and POST, "is this safe to retry?" is one of the best lenses to use. Pick the idempotent verb when you can; it's a gift to every client integrating with your API.
Resource Design
Resources are nouns, not verbs. URLs identify things, HTTP methods describe the action.
Bad:
GET /getUsers
POST /createUser
POST /deleteUser/123
Good:
GET /users → list users
POST /users → create user
GET /users/123 → get user 123
PUT /users/123 → replace user 123
PATCH /users/123 → partially update user 123
DELETE /users/123 → delete user 123
Nested resources for relationships:
GET /users/123/posts → posts by user 123
POST /users/123/posts → create post for user 123
GET /users/123/posts/456 → specific post by user 123
Don't nest deeper than 2 levels. /a/1/b/2/c/3/d/4 is unnavigable.
Pagination
Never return unbounded lists. Always paginate.
Offset pagination (simple, standard):
GET /users?page=2&limit=20
Response: { data: [...], total: 500, page: 2, totalPages: 25 }
Problem: Skipping offset=10000 still scans 10000 rows. Slow on large tables.
Cursor pagination (for feeds, high performance):
GET /users?cursor=eyJ1c2VySWQiOiI1MDB9&limit=20
Response: { data: [...], nextCursor: "eyJ1c2VySWQiOiI1MjB9" }
Benefits: O(1) regardless of position, stable results during inserts.
Keyset pagination (similar to cursor, database-native):
GET /users?after_id=500&limit=20
SQL: WHERE id > 500 ORDER BY id LIMIT 20
Use offset for admin UIs. Use cursor/keyset for public feeds and infinite scroll.
Response Shape Standards
Consistency matters more than perfection. Pick a shape and never deviate.
Success (single resource):
{ "data": { "id": "123", "name": "Alice", "email": "alice@example.com" } }
Success (list):
{
"data": [...],
"meta": { "total": 100, "page": 1, "limit": 20 }
}
Error:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [{ "field": "email", "message": "Invalid email" }]
}
}
Timestamp fields: always ISO 8601 UTC. "2024-01-15T10:30:00Z"
IDs: string (not integer) — future-proof and avoids JS precision issues.
HATEOAS
HATEOAS (Hypermedia as the Engine of Application State) is the most misunderstood REST constraint. A truly RESTful API returns links to related actions in every response:
{
"data": {
"id": "123",
"status": "pending",
"_links": {
"self": { "href": "/orders/123" },
"cancel": { "href": "/orders/123/cancel", "method": "POST" },
"payment": { "href": "/orders/123/payment" }
}
}
}
The client discovers what it can do next from the response itself — no out-of-band documentation needed. GitHub's API partially implements this.
Rarely worth full implementation. Understand the concept; apply where it adds value.
The Backend from First Principles series is based on what I learnt from Sriniously's YouTube playlist — a thoughtful, framework-agnostic walk through backend engineering. If this material helped you, please go check the original out: youtube.com/@Sriniously. The notes here are my own restatement for revisiting later.