REST API Design Principles
REST (Representational State Transfer) is an architectural style for APIs. It uses HTTP methods and URLs to define operations. A well-designed REST API is intuitive, consistent, and a pleasure to consume. A poorly designed one is confusing and error-prone.
REST's strength is its simplicity and standardization. Developers immediately understand how to interact with a REST API because they follow conventions. GraphQL offers more flexibility but less standardization. For most APIs, REST is the right choice.
Resources and URLs
REST APIs are organized around resources. A resource is a noun: users, orders, products, comments. The URL identifies the resource:
/users— collection of users/users/123— user with ID 123/orders— collection of orders/users/123/orders— orders belonging to user 123
URLs are nouns, not verbs. Bad: /getUser, /createOrder. Good: /users, /orders. The HTTP method specifies the action.
HTTP Methods
| Method | Meaning | Idempotent | Safe | Example |
|---|---|---|---|---|
| GET | Retrieve a resource | Yes | Yes | GET /users/123 |
| POST | Create a new resource | No | No | POST /users (with new user data) |
| PUT | Replace entire resource | Yes | No | PUT /users/123 (with updated user data) |
| PATCH | Partial update to resource | No | No | PATCH /users/123 (with fields to update) |
| DELETE | Remove a resource | Yes | No | DELETE /users/123 |
Idempotent: calling the method multiple times with the same request has the same effect as calling once. PUT is idempotent—calling it twice replaces the resource twice with the same value, same result. POST is not—calling it twice creates two resources.
Safe: the method doesn't modify server state. GET is safe (just retrieving). DELETE is not safe (it removes data).
HTTP Status Codes
Status codes communicate the result of the request:
- 2xx (Success):
200 OK(request succeeded),201 Created(resource created),204 No Content(request succeeded, no body) - 3xx (Redirect):
301 Moved Permanently,304 Not Modified(cached version is still valid) - 4xx (Client error):
400 Bad Request(invalid request),401 Unauthorized(authentication required),403 Forbidden(authenticated but not allowed),404 Not Found(resource doesn't exist) - 5xx (Server error):
500 Internal Server Error(something went wrong),503 Service Unavailable(temporarily down)
Using the correct status code matters. A client library might retry on 5xx but not 4xx. Proper status codes enable proper error handling.
Request and Response Bodies
JSON is the standard for API bodies. Send and receive JSON. Be consistent about structure:
POST /users
{
"email": "user@example.com",
"name": "Alice"
}
Response (201 Created):
{
"id": 123,
"email": "user@example.com",
"name": "Alice"
}
Envelope structure: should responses be wrapped in an envelope? { "data": [...] } or just the data? Either works, but be consistent.
Versioning
APIs change. New fields are added, endpoints are removed, behaviors change. How do you handle this without breaking existing clients?
Option 1: URL versioning. /api/v1/users, /api/v2/users. Clear, but requires maintaining multiple versions.
Option 2: Header versioning. Accept: application/vnd.api+json;version=1. Cleaner URLs, but less discoverable.
Option 3: No explicit versioning, but graceful evolution. New fields are additive (never break existing fields), optional parameters are accepted. Clients ignore fields they don't understand.
Plan for versioning from day one. Adding versioning retroactively is painful. Most APIs use URL versioning for simplicity.
Pagination
Never return unlimited records. Queries on large tables are slow. Send too much data and clients waste bandwidth. Paginate:
GET /users?page=1&limit=20
Response:
{
"data": [...],
"total": 1000,
"page": 1,
"limit": 20
}
Offset-based pagination (page/limit) is simple but inefficient for large offsets. Cursor-based pagination (using a cursor to mark the starting point) is more efficient for large datasets.
Error Responses
Errors need clear messaging. Bad: { "error": "Invalid input" }. Good:
{
"code": "INVALID_EMAIL",
"message": "Email must be a valid email address",
"details": { "field": "email" }
}
Include: an error code (INVALID_EMAIL, not just "error"), a message explaining the issue, optional details about which field/resource had the problem.
Authentication
APIs (consumed by non-browser clients) typically use bearer tokens in the Authorization header:
Authorization: Bearer eyJhbGc...token
The token is usually a JWT (JSON Web Token) that includes claims about the user (id, permissions) that the server can verify without hitting the database.
For browser-based clients, use secure cookies. For APIs, use bearer tokens.
Following Conventions
REST conventions reduce cognitive load for developers using your API. They immediately understand how to interact with it. Deviating from conventions confuses users and wastes time. Unless you have a strong reason, follow REST conventions.
Good API design is about making the API intuitive and predictable. A developer should be able to guess how to use your API based on what they know about REST. If they can't, you've made the API too custom.
Documentation
A brilliant API is useless without documentation. Document every endpoint: what it does, what parameters it accepts, what it returns, what errors might occur.
Tools like Swagger/OpenAPI generate interactive documentation from your API code. This is valuable for developers learning your API and for testing.