REST API Design Best Practices: A Complete Guide for Developers
A well-designed REST API is a joy to use. A poorly designed one causes bugs, confusion, and hours of wasted time for everyone who consumes it. Whether you're building your first API or reviewing an existing one, these best practices will help you design endpoints that developers actually want to work with.
What Is REST?
REST (Representational State Transfer) is an architectural style for building APIs over HTTP. It is not a protocol or standard β it is a set of constraints that, when followed, produce predictable, scalable, and easy-to-understand APIs.
The six REST constraints are: statelessness, client-server separation, cacheability, layered system, uniform interface, and (optionally) code on demand. In practice, most "REST APIs" focus on the uniform interface β consistent URLs, HTTP methods, and response formats.
Use Nouns, Not Verbs in URLs
The most common REST design mistake: putting actions in the URL.
code# Bad β verbs in URLs GET /getUsers POST /createUser PUT /updateUser/42 DELETE /deleteUser/42 # Good β nouns only, HTTP method carries the action GET /users POST /users PUT /users/42 DELETE /users/42
The HTTP method (GET, POST, PUT, DELETE) already expresses the action. The URL should describe the resource, not the operation.
Use Plural Nouns for Collections
code# Inconsistent β avoid GET /user GET /users/42/order # Consistent β plural everywhere GET /users GET /users/42 GET /users/42/orders GET /users/42/orders/7
Using plural consistently makes the API predictable. /users is a collection, /users/42 is a single item β this pattern holds everywhere.
HTTP Methods and Their Meanings
| Method | Action | Idempotent | Safe |
|---|---|---|---|
| GET | Read | Yes | Yes |
| POST | Create | No | No |
| PUT | Replace (full update) | Yes | No |
| PATCH | Partial update | No | No |
| DELETE | Delete | Yes | No |
Idempotent means calling it multiple times has the same effect as calling it once. DELETE /users/42 twice is the same as deleting once (second call might return 404, but the state is the same).
Safe means it does not modify server state.
HTTP Status Codes
Use the right status code β do not return 200 OK with {"error": "not found"} in the body.
Success codes
code200 OK β successful GET, PUT, PATCH, DELETE 201 Created β successful POST (include Location header) 204 No Content β success with no response body (DELETE)
Client error codes
code400 Bad Request β invalid input, validation failed 401 Unauthorized β not authenticated (no or bad token) 403 Forbidden β authenticated but not allowed 404 Not Found β resource does not exist 409 Conflict β duplicate resource, version conflict 422 Unprocessable β well-formed but semantically invalid 429 Too Many Requests β rate limit exceeded
Server error codes
code500 Internal Server Error β unexpected server error 502 Bad Gateway β upstream service failed 503 Service Unavailable β server down or overloaded
Consistent Error Response Format
Always return errors in a consistent, machine-readable format:
json{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ { "field": "email", "message": "Must be a valid email address" }, { "field": "age", "message": "Must be at least 18" } ] } }
Include a human-readable message, a machine-readable code, and field-level details for validation errors. This makes debugging fast for API consumers.
Versioning
APIs change over time. Versioning protects existing clients when you make breaking changes.
The most common approaches:
code# URL versioning β most visible, easiest to test in browser GET /v1/users GET /v2/users # Header versioning β cleaner URLs GET /users Accept: application/vnd.myapi.v2+json # Query parameter versioning β simple but messy GET /users?version=2
URL versioning (/v1/, /v2/) is the most widely used. It is easy to document, easy to test, and immediately obvious in logs.
Best practice: Version from day one, even if you only have v1. Retrofitting versioning later is painful.
Filtering, Sorting, and Searching
Use query parameters for these β never create separate endpoints.
code# Filtering GET /users?status=active GET /orders?status=pending&created_after=2024-01-01 # Sorting GET /users?sort=created_at&order=desc GET /products?sort=price&order=asc # Searching GET /users?search=alice # Combining GET /products?category=electronics&sort=price&order=asc&search=laptop
Pagination
Never return unlimited collections β always paginate.
Offset pagination
GET /users?page=2&per_page=20
Response:
json{ "data": [...], "pagination": { "page": 2, "per_page": 20, "total": 243, "total_pages": 13 } }
Simple but has performance issues on large datasets (database must count and skip rows).
Cursor pagination
codeGET /users?cursor=eyJpZCI6MTAwfQ&limit=20
Response:
json{ "data": [...], "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
More scalable for large datasets β no offset calculation needed. Preferred for real-time feeds (Twitter, Instagram style).
Nested Resources
Use nesting to express ownership relationships β but keep it shallow.
code# Good β one level of nesting GET /users/42/orders POST /users/42/orders GET /users/42/orders/7 # Too deep β avoid GET /users/42/orders/7/items/3/reviews/1
If nesting goes more than two levels deep, consider using a flat endpoint with filter parameters instead:
code# Instead of: GET /users/42/orders/7/items GET /order-items?order_id=7
Request and Response Design
Use JSON everywhere
codeContent-Type: application/json Accept: application/json
Return the created/updated resource in the response
httpPOST /users Content-Type: application/json { "name": "Alice", "email": "alice@example.com" }
Response 201 Created:
json{ "id": 42, "name": "Alice", "email": "alice@example.com", "created_at": "2025-03-11T10:00:00Z" }
Returning the full resource avoids a second GET request.
Use ISO 8601 for dates
json{ "created_at": "2025-03-11T10:00:00Z", "expires_at": "2025-04-11T10:00:00Z" }
Always use UTC. Never return Unix timestamps without documentation.
Authentication
Use standard, well-understood auth patterns.
JWT Bearer tokens β most common for modern APIs:
httpGET /users/me Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5...
API Keys β for server-to-server or developer access:
httpGET /data X-API-Key: sk_live_abc123...
Never put credentials in query parameters β they appear in server logs and browser history:
code# Never do this GET /users?api_key=secret123
Documentation
An undocumented API is an unusable API. Use tools like:
- OpenAPI / Swagger β industry standard, generates interactive docs
- Postman Collections β easy to share, test, and run
- Redoc β beautiful rendered OpenAPI docs
Document every endpoint: what it does, what parameters it accepts, what it returns, and what errors it can produce.
Common Interview Questions
Q: What is the difference between PUT and PATCH?
PUT replaces the entire resource. PATCH applies a partial update. If you PUT a user with only {"name": "Bob"}, the email and other fields are cleared. With PATCH, only name changes.
Q: What does idempotent mean? Which HTTP methods are idempotent?
Calling an idempotent operation multiple times produces the same result as calling it once. GET, PUT, and DELETE are idempotent. POST is not β calling POST /orders twice creates two orders.
Q: What is HATEOAS?
Hypermedia As The Engine Of Application State β responses include links to related actions and resources. A true REST constraint, but rarely implemented fully in practice.
Practice API Design Questions on Froquiz
Understanding REST is essential for backend and full-stack developers. Test yourself with our backend quizzes on Froquiz β covering API design, databases, Docker, and more.
Summary
- URLs describe resources (nouns), HTTP methods describe actions
- Use plural nouns:
/users,/orders,/products - Return correct HTTP status codes β never hide errors in 200 responses
- Consistent error format with machine-readable codes
- Version from day one:
/v1/,/v2/ - Paginate all collections β offset for simplicity, cursor for scale
- Use ISO 8601 dates, JSON everywhere, Bearer tokens for auth
- Document everything with OpenAPI