🧩 RESTful API Design Principles
HTTP gives you the words; REST gives you the grammar. This lesson turns methods and URLs into a clean, predictable, resource-oriented API — one that other developers can guess their way around because it follows consistent rules.
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what REST is and name its six architectural constraints
- Model a domain as resources and design clean, noun-based URIs
- Map CRUD operations onto the correct HTTP methods and status codes
- Handle relationships between resources with nesting vs. linking
- Apply content negotiation and describe HATEOAS and why it aids API evolution
Estimated Time: 40–50 minutes • Difficulty: Intermediate
Hands-on: Design the full URI + method + status-code map for a small domain of your choice.
In This Lesson
What REST Actually Is
REST — Representational State Transfer — is an architectural style, not a protocol or a library. Roy Fielding described it in his 2000 doctoral dissertation to capture the design principles that make the web itself scale. When we build "a REST API," we're borrowing those same principles for machine-to-machine communication.
The core idea is a shift in perspective. Instead of thinking in actions ("get the products", "create an order"), you think in resources — nouns that can be created, read, updated, and deleted using the HTTP methods you already know. The action lives in the method; the noun lives in the URL.
💡 The library-catalog analogy. A REST API is like a library catalog. Each book is a resource. Its call number is the URI that leads you to it. The things you can do — look up, add, replace, remove — map to HTTP methods. A book might come as hardcover, paperback, or e-book, just as a resource can be represented as JSON or XML. And the "see also" cross-references at the back are exactly what HATEOAS links provide.
The Six Constraints
Fielding defined REST through six constraints. Satisfy them and you get a system that is scalable, cacheable, and loosely coupled. (Code-on-demand is optional; the other five are required.)
constraints)) Client-Server Separate UI from data storage Stateless Each request is self-contained No client session on the server Cacheable Responses declare cacheability Uniform Interface Resources & URIs Standard methods Self-descriptive messages HATEOAS Layered System Proxies, gateways, load balancers Code-On-Demand Optional: send executable code
📖 The one everyone forgets: Statelessness
Each request must carry everything the server needs to handle it — including authentication. The server keeps no per-client session between requests. This is what lets you run ten identical server instances behind a load balancer: any of them can handle any request, because none of them "remembers" the client. Store session state in a token (like a JWT) or a shared store, never in the individual server's memory.
Resource-Oriented Design
The first design step is identifying your resources — the nouns of your system. For an e-commerce platform, those might be products, orders, customers, reviews, and categories. Resources aren't only database tables; they can also be collections, computed values (a sales report), or even a search result.
Every resource comes in two flavors: the collection (all products) and the item (one specific product). This distinction drives your URL design — collections are plural, items are addressed by an identifier within that collection.
Designing Good URIs
Well-designed URIs are the public face of your API. They should be predictable enough that a developer can guess the next endpoint without reading the docs.
✅ The rules
- Nouns, not verbs.
/products, never/getProducts— the HTTP method already supplies the verb. - Plural collections.
/usersand/users/42, consistently. - Hierarchy for ownership.
/users/42/ordersreads as "user 42's orders". - Query params for filtering/sorting/paging, not new paths:
/products?category=electronics&sort=price. - kebab-case for multi-word names:
/shopping-carts, not/shoppingCarts.
| Resource | Collection URI | Item URI |
|---|---|---|
| Products | /products | /products/42 |
| Orders | /orders | /orders/ORD-2026-1234 |
| A user's orders | /users/42/orders | /users/42/orders/ORD-2026-1234 |
| Order line items | /orders/ORD-2026-1234/items | /orders/ORD-2026-1234/items/5 |
⚠️ The verb-in-the-URL smell
POST /createUser, GET /deleteUser?id=5, and GET /getUserPosts are all anti-patterns — the verb belongs in the method. For genuine actions that don't map cleanly to CRUD (like cancelling an order), a sub-resource is acceptable: POST /orders/1007/cancel.
CRUD → HTTP Methods
The four database operations map directly onto HTTP methods against your two URI shapes:
| Operation | Method & URI | Success code |
|---|---|---|
| List collection | GET /products | 200 OK |
| Read one | GET /products/42 | 200 OK |
| Create | POST /products | 201 Created |
| Replace | PUT /products/42 | 200 OK |
| Partial update | PATCH /products/42 | 200 OK |
| Delete | DELETE /products/42 | 204 No Content |
Create (POST) — full request and response
POST /api/products HTTP/1.1
Host: shop.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJ...
{
"name": "Wireless Headphones",
"price": 129.99,
"category": "electronics"
}
HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/products/42
{
"id": 42,
"name": "Wireless Headphones",
"price": 129.99,
"category": "electronics",
"created_at": "2026-07-31T10:30:00Z"
}
List with paging, filtering, and sorting (GET)
GET /api/products?category=electronics&sort=price&page=1&limit=2 HTTP/1.1
Host: shop.example.com
Accept: application/json
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
{
"data": [
{ "id": 42, "name": "Wireless Headphones", "price": 129.99 },
{ "id": 17, "name": "Bluetooth Speaker", "price": 79.99 }
],
"pagination": { "total": 2, "page": 1, "limit": 2 }
}
💡 PUT vs PATCH, again
A PUT /products/42 must contain the complete product; missing fields are wiped. A PATCH /products/42 with just { "price": 139.99 } changes only the price. Reach for PATCH for everyday edits.
Handling Relationships
Real domains are webs of related resources: a user has orders, an order has items, a product belongs to categories. REST offers two ways to express these, and mature APIs mix both.
Approach 1 — Nested resources
GET /api/users/42/orders # all orders belonging to user 42
POST /api/users/42/orders # create an order for user 42
GET /api/users/42/orders/1007 # a specific order in that context
Nesting clearly expresses ownership and lets you enforce access control at the parent. The downside is depth: /companies/1/departments/3/employees/9/tasks quickly becomes unwieldy.
Approach 2 — Linking with query filters
GET /api/orders?user_id=42 # same data, flatter structure
Linking keeps URLs shallow and is far more flexible for complex queries, at the cost of making the relationship less obvious in the path.
✅ Rule of thumb
- Nest when the child only makes sense inside its parent (a comment on an article) — and cap nesting at one level.
- Use query-param linking when the resource has its own identity and you need rich filtering (
/orders?status=shipped&user_id=42). - A resource can support both:
/articles/12/commentsfor browsing,/comments/900for direct access.
Content Negotiation
The same resource can have multiple representations. Content negotiation lets the client ask for the format (and language) it wants via the Accept and Accept-Language headers — the server picks the best available match and echoes its choice in Content-Type.
GET /api/products/42 HTTP/1.1
Accept: application/json
Accept-Language: fr-FR, en;q=0.8
HTTP/1.1 200 OK
Content-Type: application/json
Content-Language: fr-FR
{ "id": 42, "nom": "Casque sans fil", "prix": 129.99 }
Here's how you'd honor the Accept header in Express:
app.get('/api/products/:id', (req, res) => {
const product = db.get(req.params.id);
if (!product) return res.status(404).json({ code: 'NOT_FOUND' });
res.format({
'application/json': () => res.json(product),
'application/xml': () => res.type('application/xml').send(toXml(product)),
default: () => res.json(product) // sensible fallback
});
});
📖 The q value
Accept-Language: fr-FR, en;q=0.8 means "French preferred, but English at 80% preference is fine." Those q (quality) weights let the client rank its preferences, and the server chooses the highest one it can satisfy.
HATEOAS & API Evolution
HATEOAS — Hypermedia As The Engine Of Application State — is the most overlooked REST constraint. The idea: responses include links that tell the client what it can do next, so the client discovers the API as it goes instead of hard-coding every URL.
It works exactly like browsing a website: you don't memorize every URL, you follow links from page to page. A HATEOAS response does the same for a machine client.
Without HATEOAS — the client must already know every URL
{
"id": "1007",
"total": 99.99,
"status": "shipped",
"customer_id": "42"
}
With HATEOAS — the response is self-describing
{
"id": "1007",
"total": 99.99,
"status": "shipped",
"_links": {
"self": { "href": "/api/orders/1007" },
"customer": { "href": "/api/customers/42" },
"items": { "href": "/api/orders/1007/items" },
"return": { "href": "/api/orders/1007/return", "method": "POST" }
}
}
Notice the return link appears only because the order is shipped. State-appropriate links mean the server can guide the client through valid actions — and can add or move endpoints later without breaking clients that follow links instead of building URLs by hand.
⚠️ When breaking changes force a new version
Even with hypermedia, some changes break clients: removing or renaming a field, changing a field's type, or making a previously optional field required. Those need a new API version (commonly /api/v2/...). Non-breaking changes — adding a new endpoint or an optional response field — do not. We'll go deep on versioning in the next lesson.
Hands-on Exercise
🏋️ Design a REST API for a library
Objective: Turn a small domain into a clean, resource-oriented API on paper.
Instructions
- Identify the resources for a library system: at minimum books, patrons, and loans.
- For each resource, write the collection URI and item URI, and list which HTTP methods apply.
- Model at least one relationship (a patron's loans) both ways — nested and linked — and note which you'd choose.
- Design the "check out a book" action (it's not plain CRUD) and the "list books currently on loan to a patron" query.
- For one endpoint, write a full request + response with realistic status codes.
💡 Hint
"Check out" is a state transition, so a POST to a sub-resource fits: POST /loans with a body of { "book_id": 5, "patron_id": 42 }, or POST /books/5/loans. Trying to borrow an already-borrowed book is a great use for 409 Conflict.
✅ Example answer (excerpt)
GET /books # list/search books (?author=, ?available=true)
GET /books/5 # one book
POST /loans # check out a book
DELETE /loans/900 # return a book (or PATCH status)
GET /patrons/42/loans # a patron's current loans
POST /loans
{ "book_id": 5, "patron_id": 42 }
--> 201 Created
Location: /loans/900
{ "id": 900, "book_id": 5, "patron_id": 42, "due": "2026-08-14" }
--> 409 Conflict (if the book is already on loan)
{ "code": "BOOK_UNAVAILABLE", "message": "That book is checked out." }
🎯 Quick Quiz
Question 1: Which URL best follows REST conventions for fetching all orders belonging to user 42?
Question 2: What does the "stateless" constraint require?
Question 3: What problem does HATEOAS primarily solve?
Summary & Quiz
🎉 Key Takeaways
- REST is a style built on resources and the uniform HTTP interface — think nouns, not actions.
- Its six constraints (client-server, stateless, cacheable, uniform interface, layered, optional code-on-demand) are what make it scale.
- Design URIs as plural nouns with hierarchy for ownership and query params for filtering/sorting/paging.
- CRUD maps to methods: GET/POST/PUT/PATCH/DELETE with specific success codes.
- Express relationships via nesting or linking; support content negotiation; and use HATEOAS links to keep clients loosely coupled.
📚 Further Reading
- Roy Fielding — REST APIs must be hypertext-driven
- JSON:API — a conventions specification
- MDN — HTTP content negotiation
- Best Practices for a Pragmatic RESTful API
🚀 What's Next?
You can now design a well-structured API. Next, API Design Best Practices polishes it: consistent error formats, versioning strategies, documentation with OpenAPI, authentication choices, caching, and performance.
🎉 Great progress!
Your APIs now have a shape other developers can trust. Let's make them a joy to use.