Designing API Endpoints
Good endpoint design is mostly naming discipline. A consistent, predictable URL scheme means clients can guess endpoints correctly and your API documents itself. This page covers the conventions the industry has converged on: plural nouns, nesting for relationships, when to nest vs. flatten, and how to handle the operations that don't fit neatly into CRUD.
Use plural nouns for collections
Good | Avoid | Why |
|---|---|---|
|
| A collection is plural |
|
| No verb in the path |
|
| Method is the verb |
|
| Model the relationship |
Nesting for relationships
GET /users/5/orders → orders belonging to user 5 POST /users/5/orders → create an order for user 5 GET /users/5/orders/9 → a specific order of user 5 GET /articles/42/comments → comments on article 42
Filtering, sorting, paging go in the query string
GET /products?category=books&minPrice=10 ← filter GET /products?sort=-price,name ← sort (- = descending) GET /products?page=2&limit=20 ← paginate GET /products?fields=id,name,price ← sparse fieldset GET /users?q=ada ← search
Actions that aren't CRUD
Some operations — "publish", "cancel", "send email" — don't map cleanly onto create/read/update/delete. Two acceptable patterns:
// 1. Model the action as a sub-resource state change (preferred):
POST /articles/42/publish ← a controller-style action
PATCH /articles/42 { "status": "published" } ← or a state field
// 2. Model it as a sub-collection when it creates something:
POST /orders/9/refunds ← creating a refund resourceURL style rules
Lowercase paths —
/userProfilesand/userprofilesmay differ on some servers; stick to lowercase.Hyphens, not underscores or camelCase, for multi-word paths —
/order-items.No trailing slash convention — pick one (usually none) and redirect the other.
No file extensions —
/users, not/users.json; use theAcceptheader for format.Version at the root —
/v1/users(see API versioning).
A worked resource: blog posts
Method & path | Action |
|---|---|
| List posts (paginated, filterable) |
| Create a post |
| Fetch one post |
| Replace a post |
| Partially update a post |
| Delete a post |
| List comments on a post |
| Add a comment to a post |
Mapping the design onto an Express router
import { Router } from 'express'
const posts = Router()
posts.get('/', listPosts)
posts.post('/', createPost)
posts.get('/:id', getPost)
posts.put('/:id', replacePost)
posts.patch('/:id', updatePost)
posts.delete('/:id', deletePost)
posts.get('/:id/comments', listComments)
posts.post('/:id/comments', addComment)
// app.use('/v1/posts', posts)
export default posts