REST API Design: How to Not Make Everyone Hate Your API
Practical guide to designing REST APIs that don't suck. Resource naming, HTTP methods, status codes, pagination, error formats, and the anti-patterns you see everywhere.
You can tell a lot about a backend team by their API design. Good APIs are predictable -- you can guess the endpoint for something you haven't used yet. Bad APIs make you check the docs for every single request because nothing is consistent. Here's how to design APIs that developers actually enjoy using.
Resources, Not Actions
REST is about resources (nouns), not actions (verbs). The HTTP method IS the verb.
# Good
GET /users -- list users
GET /users/42 -- get user 42
POST /users -- create a user
PUT /users/42 -- replace user 42
PATCH /users/42 -- partially update user 42
DELETE /users/42 -- delete user 42
# Bad
GET /getUsers
POST /createUser
POST /deleteUser/42
GET /getUserById?id=42
If you have verbs in your URLs, something's wrong. The exception is truly action-oriented operations that don't map to CRUD (like /users/42/deactivate), but even those can often be modeled as state changes: PATCH /users/42 with {"status": "inactive"}.
Nested Resources
Use nesting to express relationships:
GET /users/42/posts -- posts by user 42
GET /users/42/posts/7 -- post 7 by user 42
POST /users/42/posts -- create a post for user 42
Don't go deeper than two levels. /users/42/posts/7/comments/3/replies/1 is a nightmare. Flatten it: /replies/1 or /comments/3/replies.
Pluralize Everything
/users not /user
/posts not /post
/categories not /category
Pick plural and be consistent. Having /user/42 but /posts will annoy everyone.
HTTP Methods: Use Them Correctly
| Method | Purpose | Idempotent? | Safe? |
|---|---|---|---|
| GET | Read | Yes | Yes |
| POST | Create | No | No |
| PUT | Full replace | Yes | No |
| PATCH | Partial update | Yes* | No |
| DELETE | Remove | Yes | No |
DELETE /users/42 twice? User is still deleted. POST /users twice? You might get two users. This distinction matters for retry logic.
PUT vs PATCH: PUT replaces the entire resource (you send all fields). PATCH sends only the fields you want to change. Most of the time, PATCH is what you actually want.
Status Codes That Matter
Stop returning 200 for everything. Status codes exist for a reason, and clients depend on them.
Success codes:200 OK-- general success, response has a body201 Created-- resource was created (return the created resource, plus aLocationheader)204 No Content-- success, but nothing to return (good for DELETE)
400 Bad Request-- malformed request, validation error, missing required field401 Unauthorized-- not authenticated (should really be called "Unauthenticated")403 Forbidden-- authenticated but not allowed404 Not Found-- resource doesn't exist409 Conflict-- state conflict (duplicate email, version mismatch)422 Unprocessable Entity-- request is well-formed but semantically invalid
500 Internal Server Error-- something broke on your end503 Service Unavailable-- temporarily down (includeRetry-Afterheader)
Error Response Format
Pick a format and use it everywhere:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address"
},
{
"field": "age",
"message": "Must be a positive integer"
}
]
}
}
Key points:
- Machine-readable error code (
VALIDATION_ERROR, not just a string message) - Human-readable message for debugging
- Field-level details for validation errors (so the frontend can show errors next to the right form field)
- Same structure for every error endpoint. No guessing.
Pagination
Any endpoint that returns a list needs pagination. Returning 50,000 records because someone forgot to add ?limit=20 will take down your server.
GET /posts?page=2&per_page=20
{
"data": [...],
"pagination": {
"page": 2,
"per_page": 20,
"total": 143,
"total_pages": 8
}
}
Cursor-based (better for real-time data):
GET /posts?after=eyJpZCI6MTAwfQ&limit=20
{
"data": [...],
"pagination": {
"next_cursor": "eyJpZCI6MTIwfQ",
"has_more": true
}
}
Cursor-based avoids the "page drift" problem where items shift between pages as new content is added. Use it for feeds, timelines, or anything where data changes frequently.
Filtering and Sorting
Keep it simple and consistent:
GET /posts?status=published&author_id=42&sort=-created_at
- Filter by field:
field=value - Sort:
sort=field(ascending) orsort=-field(descending) - Multiple sort fields:
sort=-created_at,title
GET /products?price[gte]=10&price[lte]=50
GET /products?created_at[after]=2026-01-01
Don't invent your own query language. If you need complex queries, consider GraphQL instead.
Common Anti-Patterns
Inconsistent naming./users uses camelCase (firstName) but /products uses snake_case (product_name). Pick one. snake_case is the most common convention in JSON APIs.
Version in the URL path. /v1/users is fine. /api/v2/users is fine. Just don't mix versioned and unversioned endpoints, and don't break v1 when you ship v2.
Returning different shapes for the same resource. If GET /users/42 returns {id, name, email} but GET /users returns [{userId, fullName, emailAddress}], that's a bug. Same resource, same field names, always.
Ignoring HTTP caching. Set Cache-Control headers on GET responses. Use ETag or Last-Modified for conditional requests. Your API might be fast, but HTTP caching makes it faster and reduces load.
Not including the created resource in POST responses. When someone creates a resource, return it. They need the server-generated ID, timestamps, and any defaults you applied. Don't make them do a separate GET request immediately after creating something.
Getting Started
The best way to internalize good API design is to build some APIs yourself. If you're learning backend development, CodeUp gives you a hands-on environment to practice building and consuming APIs. Once you've felt the pain of a badly designed API from the consumer side, you'll never build one yourself.
Good API design isn't about following rules for their own sake. It's about making your API predictable enough that developers can use it without constantly referring to the docs. Consistency beats cleverness every time.