REST API Design: Stop Guessing, Start Following Patterns
Most backend engineers write APIs. Few design them. The difference shows up every time someone integrates your work and either breezes through it — or spends half a day decoding inconsistencies.
This is the complete guide to REST API design: where it came from, what the patterns are, and how to make decisions confidently every time.
Where REST Came From
In 1990, Tim Berners-Lee built the World Wide Web to share knowledge globally. He invented URI, HTTP, HTML, the first web server, and the first web browser — all within roughly a year.
The web grew faster than he planned. Way faster.
Around 1993, Roy Fielding — co-founder of the Apache HTTP Server project — became concerned about scalability. The infrastructure wasn't built for the scale of users arriving daily. His solution: six architectural constraints that would make the web scalable by design.
| Constraint | What It Does |
|---|---|
| Client-Server | Separates UI from business logic. They evolve independently. |
| Stateless | Each request carries all information needed. Server holds no client context. |
| Caching | Responses labeled cacheable or not. Reduces load, improves speed. |
| Uniform Interface | Standardizes how components communicate. |
| Layered System | Each layer only sees the one directly below. Enables proxies, load balancers. |
| Code on Demand | Optional. Servers can send executable code (JavaScript) to clients. |
Fielding and Berners-Lee co-authored the HTTP/1.1 spec applying these constraints. Then in 2000, Fielding published his PhD dissertation formally naming this architectural style: Representational State Transfer — REST.
Every "REST API" traces back to that document.
What REST Actually Means
The acronym breaks down into three real ideas:
Representational — resources can be expressed in multiple formats. The same user resource becomes JSON for API clients, HTML for browsers. The representation changes based on context; the resource doesn't.
State — each resource has a current state (its attributes and values). A shopping cart's state: items, quantities, price. This state is what gets transferred.
Transfer — the movement of resource representations between client and server via HTTP.
Everything else — routes, methods, status codes — is practical implementation of this idea.
URL Structure
A well-designed API URL:
https://api.example.com/v1/organizations/42/projects
| Part | Example | Purpose |
|---|---|---|
| Scheme | https |
Always HTTPS in production |
| Subdomain | api. |
Separates API from the main site |
| Domain | example.com |
Your service |
| Version | /v1 |
Allows breaking changes without breaking existing clients |
| Resources | /organizations/42/projects |
The hierarchy of what you're accessing |
Resources Are Always Plural
Always. Without exception.
✅ GET /books
✅ GET /books/42
✅ POST /books
❌ GET /book
❌ GET /book/42
The resource name refers to the collection. You're always operating within that collection — whether listing all or targeting one by ID. Singular creates inconsistency. Plural is consistent regardless.
Slashes Represent Hierarchy
/organizations → all organizations
/organizations/42 → organization 42
/organizations/42/projects → all projects in that org
/organizations/42/projects/7/tasks → all tasks in that project
Read a route top-to-bottom and the context is obvious without documentation.
URL Formatting Rules
All lowercase
Spaces → hyphens (
harry-potter, notharry_potter)Never underscores, never spaces
✅ /books/harry-potter-and-the-philosopher-stone
❌ /books/Harry_Potter_And_The_Philosopher_Stone
HTTP Methods — Semantics Matter
Methods exist to express intent. They answer: "What are you trying to do?"
GET /projects → fetch all projects
POST /projects → create a new project
PATCH /projects/42 → partially update project 42
PUT /projects/42 → completely replace project 42
DELETE /projects/42 → delete project 42
| Method | Intent | Has Body? | Idempotent? |
|---|---|---|---|
| GET | Retrieve | No | ✅ |
| POST | Create / custom action | Yes | ❌ |
| PUT | Full replacement | Yes | ✅ |
| PATCH | Partial update | Yes | ❌* |
| DELETE | Remove | Optional | ✅ |
PATCH vs PUT — Know the Difference
// Existing record:
{
"id": 1,
"name": "Design System",
"status": "active",
"description": "Our component library",
"owner": "priya@company.com"
}
// PATCH /projects/1
// Request: { "status": "archived" }
// Result: only status changes. Everything else stays.
{
"id": 1,
"name": "Design System",
"status": "archived",
"description": "Our component library",
"owner": "priya@company.com"
}
// PUT /projects/1
// Request: { "status": "archived" }
// Result: everything else is GONE.
{
"id": 1,
"status": "archived"
}
Default to PATCH. Use PUT only when full replacement is intentionally required — which is rare.
Idempotency — The Property That Matters at Scale
Idempotent = calling the same operation multiple times has the same effect as calling it once. Not the same response — the same side effects on the server.
GET /projects/42 → data returned, nothing changed on server
GET /projects/42 → same ✅
GET /projects/42 → same ✅
DELETE /projects/42 → project deleted
DELETE /projects/42 → 404 (already gone, but nothing else changed) ✅
POST /projects → Project #1 created
POST /projects → Project #2 created ❌ (different result)
POST /projects → Project #3 created ❌
Why It Actually Matters
Networks fail. Requests time out. When a request fails mid-flight, should you retry?
Idempotent operation → retry safely. Same outcome.
Non-idempotent operation (
POST) → retrying might create duplicates.
This is why payment systems use idempotency keys — a unique token sent with each request so the server can recognize and ignore duplicates. One missed detail here = customers charged twice.
Designing Your Resources
Before writing routes, identify your resources. A resource is any noun that represents data your system manages.
Workflow: go through your UI designs / wireframes → extract every noun that represents managed data → those are your resources.
For a project management platform (Jira/Linear-style):
Organizations → companies on the platform
Projects → work containers inside an org
Tasks → units of work within a project
Users → people using the platform
Tags → labels for organizing tasks
Comments → discussion on tasks
Each resource gets its own set of routes. All follow the same patterns.
The CRUD Pattern
Every resource will have some combination of five operations. The pattern is always the same:
| Operation | Method | Route | Status Code |
|---|---|---|---|
| Create | POST | /organizations |
201 Created |
| List | GET | /organizations |
200 OK |
| Get one | GET | /organizations/:id |
200 OK |
| Update | PATCH | /organizations/:id |
200 OK |
| Delete | DELETE | /organizations/:id |
204 No Content |
Create and List share the same route — the method differentiates them. Get, Update, and Delete share the same route structure — again, method differentiates them.
Create
POST /organizations
Content-Type: application/json
{
"name": "Acme Corp",
"description": "Building better anvils since 1952"
}
HTTP/1.1 201 Created
{
"id": "org_9f2a3b",
"name": "Acme Corp",
"description": "Building better anvils since 1952",
"status": "active",
"createdAt": "2026-03-11T10:00:00Z"
}
201, not 200. id, createdAt, status are server-set — don't ask the client for them. All JSON fields in camelCase.
List
GET /organizations
HTTP/1.1 200 OK
{
"data": [...],
"total": 47,
"page": 1,
"totalPages": 5
}
Empty list? Still 200:
{ "data": [], "total": 0, "page": 1, "totalPages": 0 }
Never 404 for an empty list. 404 = a specific resource you requested doesn't exist. An empty list is a valid, successful response.
Update
PATCH /organizations/org_9f2a3b
{ "description": "Updated description" }
HTTP/1.1 200 OK
{ ...full updated resource... }
Return the complete updated object. Client sent partial data — give them back the full current state.
Delete
DELETE /organizations/org_9f2a3b
HTTP/1.1 204 No Content
(empty body)
204 — success, nothing to return. Resource is gone.
Pagination, Sorting, Filtering
Every list endpoint needs all three. Without them, your API breaks under real data volumes.
Pagination
GET /organizations?page=2&limit=10
{
"data": [...10 orgs...],
"total": 47,
"page": 2,
"totalPages": 5
}
Without pagination, a 50,000-record database table means serializing 50,000 records, transmitting them, and forcing the client to parse all of it — for a user who might see 20 on screen.
Always set sane defaults server-side. If no page or limit is sent, default to page=1, limit=10. Never force clients to send obvious parameters.
Sorting
GET /organizations?sortBy=name&sortOrder=asc
Two parameters: sortBy (which field) and sortOrder (asc or desc).
Always have a default sort. Without one, result order varies between calls as databases return records in arbitrary sequence. Default: sortBy=createdAt&sortOrder=desc (latest first) is almost always the right call.
Filtering
GET /organizations?status=active
GET /organizations?status=active&name=acme
Filter params map to resource fields. Multiple filters combine. The client asks for exactly what it needs.
All Three Together
GET /organizations?page=1&limit=10&sortBy=createdAt&sortOrder=desc&status=active
This is the full pattern. Every list endpoint should support it.
Custom Actions — When CRUD Isn't Enough
Some operations don't fit create, read, update, or delete.
Example: archiving an organization.
Sounds like PATCH /organizations/42 with { "status": "archived" }. But archiving might also need to:
Deactivate all projects under that org
Send notification emails to all members
Archive all tasks in those projects
Log an audit event
That's not an update. It's an action. A simple status patch doesn't carry that semantic weight.
Use POST with the action name:
POST /organizations/42/archive
POST /projects/7/clone
POST /tasks/3/assign
POST /invoices/12/send
Structure:
/organizations → the collection
/organizations/42 → a specific resource
/organizations/42/archive → an action on that resource
Response for archive (no new resource created):
HTTP/1.1 200 OK
{ ...updated organization... }
Response for clone (new resource created):
HTTP/1.1 201 Created
{ ...newly cloned project... }
The response code follows what actually happened on the server, not the method. Don't assume POST always returns 201.
Response Design
Success Responses
Single resource:
{
"id": "org_9f2a3b",
"name": "Acme Corp",
"status": "active",
"createdAt": "2026-03-11T10:00:00Z"
}
Collection:
{
"data": [...],
"total": 47,
"page": 1,
"totalPages": 5
}
Error Responses
Standardize these early. Inconsistent error shapes are one of the biggest frustrations when integrating an API.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body contains invalid fields",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "name", "message": "Required field missing" }
]
}
}
code = machine-readable. Clients branch logic on this. message = human-readable. For developers debugging. details = optional. Field-level errors for validation failures.
Status Code Cheat Sheet
| Scenario | Code |
|---|---|
| Successful GET or PATCH | 200 OK |
| Successful POST (new resource) | 201 Created |
| Successful DELETE | 204 No Content |
| Custom action, no new resource | 200 OK |
| Invalid request / missing fields | 400 Bad Request |
| Missing or expired token | 401 Unauthorized |
| Authenticated, not authorized | 403 Forbidden |
| Specific resource not found | 404 Not Found |
| Empty list result | 200 OK (not 404) |
| Duplicate / state conflict | 409 Conflict |
| Valid format, invalid data | 422 Unprocessable Entity |
| Rate limit hit | 429 Too Many Requests |
| Server crash | 500 Internal Server Error |
The Consistency Rules
This is the thing that separates good APIs from painful ones.
Plural everywhere. Not sometimes. Not mostly. Always.
camelCase for all JSON. Not snake_case. Not PascalCase. camelCase. Every payload, every response.
✅ { "createdAt": "...", "organizationId": "...", "totalPages": 5 }
❌ { "created_at": "...", "organization_id": "...", "total_pages": 5 }
No abbreviations. description not desc. organization not org. Whoever integrates your API doesn't have your mental context.
Sane defaults on everything. Don't force clients to send obvious parameters. If page isn't sent, default to 1. If sortOrder isn't sent, default to desc.
Consistent error shapes. If one endpoint returns { "error": { "message": "..." } }, they all do.
Consistent field names across resources. If one resource uses description, the next one doesn't get to use summary or desc. When a frontend engineer integrates their first endpoint, they make assumptions about your other endpoints. Make those assumptions correct.
The Design Workflow
Here's the right order of operations before writing any implementation code:
1. Understand the UI — look at wireframes / Figma designs. This is where your requirements live.
2. Identify resources — extract nouns from requirements. Those are your resources.
3. Design the database schema — map resources to tables. Think relationships and foreign keys.
4. Design the API interface in a client tool — open Postman or Insomnia. Create every request. Define routes, methods, payloads, expected responses. This is design, not implementation.
5. Write the code — now implement. Every interface decision is already made.
This sounds slower. It's faster. Design mistakes caught at step 4 take minutes to fix. Design mistakes caught at step 5+ take hours.
Interview Questions
"What is REST?" An architectural style for networked APIs defined by Roy Fielding in his 2000 PhD dissertation. Not a protocol. Six constraints: client-server, stateless, cacheable, uniform interface, layered system, code on demand (optional).
"Why are resource names plural?" They refer to collections. Plural works consistently — whether listing all items or targeting one by ID. Singular creates inconsistency.
"PATCH vs PUT?" PUT replaces the entire resource. Fields you don't send get deleted. PATCH does a partial update — only sent fields change. Default to PATCH.
"What is idempotency?" Calling an operation N times has the same server-side effect as calling it once. Matters for retry logic. GET, PUT, DELETE are idempotent. POST is not.
"When do you use POST for non-create operations?" For custom actions that don't fit CRUD — POST /organizations/42/archive, POST /projects/7/clone. POST is open-ended in the REST spec for exactly this reason.
"404 vs empty array on a list endpoint?" Empty list → 200 OK with "data": []. 404 is for when a specific requested resource doesn't exist. An empty list is a valid successful response.
"What's in a paginated response?" data (current page's records), total (count across all pages), page (current page number), totalPages (total pages count).
"Status code for DELETE?" 204 No Content. Resource is gone, nothing meaningful to return.
This is from series Backend Engineering from First Principles. Next: Mastering Databases.

