REST API Design: A Mental Model for Backend Engineers
You'll spend more time designing APIs than almost any other backend task. A well-designed API is invisible — developers integrate and move on. A poorly designed one generates Slack messages at 2 AM asking "what does this endpoint even return?"
This is the seventh fundamental I'm revisiting in my backend-from-first-principles series. All the previous topics — HTTP, routing, serialization, auth, validation, architecture — converge here. REST API design is where you put it all together into something other developers (and your future self) actually use.
What REST Actually Means
REST stands for Representational State Transfer. Let's break those three words apart because they hold the entire philosophy:
- Representational — A resource (like a user or a product) can have multiple representations. An API client gets JSON. A browser gets HTML. The underlying data is the same; the format changes based on who's asking.
- State — The current condition of a resource. A shopping cart has items, quantities, and a total price at any given moment. That's its state.
- Transfer — Moving those representations between client and server using standard HTTP methods.
REST was proposed by Roy Fielding in 2000 to solve the web's scalability crisis. It's not a library or a framework — it's a set of architectural constraints. Follow them, and your system scales. Violate them, and you're swimming upstream.
The Six Constraints
A truly RESTful system follows these rules:
- Client-Server — Strict separation. Client handles UI, server handles data. (We covered this in the HTTP article.)
- Stateless — Server retains no memory between requests. Every request is self-contained. (Also covered in HTTP.)
- Cacheable — Responses must explicitly declare if they're cacheable or not. (The caching conversation from HTTP.)
- Uniform Interface — Everyone communicates the same way. Same URL patterns, same methods, same conventions.
- Layered System — You can insert proxies, load balancers, and gateways between client and server. Each layer only sees the layer directly below it.
- Code on Demand (optional) — Servers can send executable code (like JavaScript) to extend client functionality.
Notice how constraints 1-3 are things we've already internalized from previous topics. REST is the architectural style that ties everything together.
The Mental Model: Designing a Library System
Imagine you're designing the card catalog for a public library. Every book, every member, every loan needs to be findable through a consistent, predictable system.
You wouldn't label one shelf "getBooks" and another "fetch_all_members" and another "LOANS-list." You'd create a uniform system where anyone can walk in and find anything without asking for help.
That's REST API design — building a uniform, predictable, self-documenting interface that any developer can navigate without reading your source code.
Anatomy of a RESTful URL
Every REST API URL follows a consistent structure:
https://api.example.com/v1/organizations/123/projects https → Scheme (always HTTPS) api. → API subdomain example.com → Domain v1 → API version organizations → Resource (plural noun) 123 → Specific resource ID projects → Nested resource
The Naming Rules
These rules aren't suggestions — they're the difference between an API that feels intuitive and one that drives developers insane:
- Always use plural nouns.
/users,/projects,/organizations. Even when fetching a single item:/users/123— not/user/123. The collection stays plural; the ID narrows it down. - Never use verbs in URLs. The HTTP method IS the verb.
- Wrong:
GET /getUsersorPOST /createUser - Right:
GET /usersorPOST /users
- Wrong:
- Use hyphens for readability.
/project-members— not/project_membersor/projectMembers. URLs are lowercase with hyphens. - Use camelCase in JSON payloads.
{ "firstName": "Ali", "createdAt": "..." }. URLs use hyphens, JSON uses camelCase. Be consistent. - Use
/to express hierarchy./organizations/123/projectsreads as "projects belonging to organization 123." Each slash zooms one level deeper.
Methods + Resources: The Complete CRUD Map
The HTTP method declares the action. The URL declares the resource. Together, they form a sentence:
GET /projects → "List all projects" GET /projects/123 → "Get project 123" POST /projects → "Create a new project" PATCH /projects/123 → "Partially update project 123" PUT /projects/123 → "Fully replace project 123" DELETE /projects/123 → "Delete project 123"
Six endpoints. No ambiguity. Any developer can look at these and immediately understand what they do. That's the power of a uniform interface.
PATCH vs PUT: The Distinction That Matters
PATCH — Partial update. Send only the fields you want to change:
PATCH /projects/123
{ "status": "archived" }
→ Only the status field changes. Everything else stays the same.
PUT — Full replacement. Send the entire resource. Anything you don't include gets wiped:
PUT /projects/123
{ "name": "New Name", "status": "active", "description": "..." }
→ The entire resource is replaced with this payload.
In practice, PATCH is used far more often because most updates are partial — changing a status, updating a name, toggling a setting.
Idempotency Recap
This was covered in the HTTP article, but it's essential context for API design:
- GET, PUT, PATCH, DELETE — Idempotent. Running them multiple times produces the same server state.
- POST — NOT idempotent. Each call creates a new resource. Send the same POST 10 times, get 10 entries in your database.
This is why POST is used for creation and all other actions that need to happen exactly once unless protected by idempotency keys.
Custom Actions: When CRUD Isn't Enough
Sometimes an action doesn't fit neatly into Create, Read, Update, or Delete. Archiving an organization, cloning a project, or sending an email — these trigger complex workflows beyond a simple database write.
The REST convention:
- Use POST as the method
- Append the action as a verb at the end of the resource URL
POST /projects/123/clone → Clone project 123 POST /organizations/5/archive → Archive organization 5 POST /users/42/send-invite → Send invite to user 42
This is the one exception where a verb appears in the URL. It's acceptable because the action genuinely doesn't map to CRUD. Don't abuse it — if an action can be expressed as a PATCH (like changing a status field), use PATCH instead.
Designing List APIs: Pagination, Sorting, and Filtering
This is where API design gets practical. A GET /users endpoint that returns 50,000 records will crash mobile clients, bottleneck networks, and make serialization/deserialization painfully slow.
Pagination — Serving Data in Chunks
Instead of returning everything, return one page at a time:
GET /organizations?page=2&limit=20
The response includes metadata so the client knows where it stands:
{
"data": [ ... 20 organizations ... ],
"total": 150,
"page": 2,
"totalPages": 8
}
A production note: This offset-based pagination (page=2&limit=20) works well for small-to-medium datasets. But at scale (100K+ records), it degrades — the database still scans through all skipped rows. For large datasets and real-time feeds, cursor-based pagination maintains constant performance by using an opaque pointer to the last seen record instead of a page number. You'll encounter both patterns.
Sorting — Let Clients Control Order
GET /organizations?sortBy=name&sortOrder=asc
The client specifies which field to sort by and the direction. Without explicit sorting, databases don't guarantee order. Consecutive requests to the same endpoint might return rows in different sequences.
Filtering — Narrow Down Results
GET /organizations?status=active&name=acme
Filter by any relevant field. This is how list APIs handle search and discovery — query parameters act as filters on the collection.
Combining Everything
A real-world list API call often combines all three:
GET /organizations?page=1&limit=10&sortBy=createdAt&sortOrder=desc&status=active
Page 1, 10 results, sorted newest first, only active organizations. Clean, readable, predictable.
Sane Defaults: The Defensive Design Pattern
Your server should never crash or behave unpredictably because a client forgot an optional parameter. This is where "sane defaults" come in:
- No
pagesent? → Default to1 - No
limitsent? → Default to10(and cap at a max like100to prevent abuse) - No
sortBysent? → Default tocreatedAt - No
sortOrdersent? → Default todesc(newest first) - Creating an organization without a
status? → Default to"active"
Sane defaults mean the API works correctly with zero query parameters. Every parameter becomes an optional refinement, not a requirement.
Status Codes: The Ones That Matter for REST
We covered all status codes in the HTTP article. For REST API design specifically, these are the rules:
200 OK— Successful GET, PATCH, PUT, or custom action201 Created— Successful POST that created a new resource. Always201, never200for creation.204 No Content— Successful DELETE. The resource is gone; there's nothing to return.400 Bad Request— Client sent invalid data (validation failure)404 Not Found— Specific resource ID doesn't exist
The 404 vs 200 Rule
This trips up a lot of engineers:
GET /users/999→ User 999 doesn't exist →404 Not Found. The specific resource can't be located.GET /users?name=Zack→ No users named Zack →200 OKwith{ "data": [] }. The collection exists; the filter returned empty results.
A 404 means "this thing doesn't exist." An empty list means "this thing exists, but nothing matches your criteria right now." They're semantically different.
The Design-First Workflow
One of the most practical insights from this topic: don't start by writing code. Start from the UI.
- Look at the wireframes/Figma. What data does the user see? What can they interact with?
- Extract the nouns. Users, Projects, Organizations, Tasks, Tags — these become your resources.
- Design the database schema. How do these resources relate to each other?
- Define CRUD actions. Which resources need create, read, update, delete?
- Design the API interface. Map resources to URLs, methods to actions, query params to filters.
- Document with Swagger/OpenAPI. Generate an interactive playground before writing a single line of backend code.
- Then write code.
This workflow prevents a common trap: building an API that serves the database structure instead of the user experience. The frontend defines what data is needed; the backend figures out how to serve it.
Response Structure: Consistency Is Everything
Every response from your API should follow the same shape. Always.
Success:
{
"data": { ... },
"message": "success"
}
List success:
{
"data": [ ... ],
"total": 50,
"page": 1,
"totalPages": 5
}
Error:
{
"error": {
"message": "Organization name is required",
"code": "VALIDATION_ERROR"
}
}
A frontend developer should never have to guess whether the response wraps data in data, result, payload, or response. Pick one structure and use it everywhere. Inconsistency is the number one source of integration bugs.
The Golden Rules Checklist
Pin this somewhere visible when designing APIs:
- Plural nouns in URLs —
/users, not/user - No verbs in URLs — HTTP methods are the verbs (except custom actions)
- camelCase in JSON —
createdAt, notcreated_at - Hyphens in URLs —
/project-members, not/project_members - Always version your API —
/v1/,/v2/ - Sane defaults for everything — no parameter should crash the server
- Consistent response structure — same shape, every endpoint, no exceptions
- 201 for creation, 204 for deletion — not 200 for everything
- Empty array, not 404, for empty list results
- Pagination on every list endpoint — no exceptions, even if you "only have 10 items" today
- Document with Swagger/OpenAPI — if it isn't documented, it doesn't exist
The Mental Model Summary
Carry this forward:
- REST = An architectural style, not a technology. Constraints that make systems scalable.
- URL = A sentence. The method is the verb, the resource is the noun.
GET /users/123reads as "get user 123." - CRUD = Four operations (Create, Read, Update, Delete) mapped to four methods (POST, GET, PATCH/PUT, DELETE). Custom actions use POST with a verb appended.
- List APIs = Always paginate, always sort, always filter. Never return unbounded data.
- Sane defaults = The API works with zero parameters. Every parameter is a refinement.
- Consistency = Same naming, same response shapes, same conventions. A developer should predict your API's behavior without reading docs.
- Design-first = Start from the UI, extract nouns, design the interface, document it, then code.
A great API is like a great library catalog — anyone can walk in, understand the system in minutes, and find exactly what they need. That's the benchmark. If a developer integrating your API needs to ask you questions, the design has failed.
This is part 7 of my "Backend from First Principles" series. Everything in this article comes from what I learned watching the Backend from First Principles playlist by Sriniously — specifically the video "Complete REST API Design." I'm not claiming original research. This is me taking what I studied, internalizing it, and presenting it as a mental model that made the concepts stick for me. If it helps you too, even better.
Follow along for the next one.