You can get the routing, methods, and status codes perfectly right and still ship an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. that's painful to work with, because the response bodies are inconsistent, error messages are unstructured, or paginationWhat is pagination?Splitting a large set of results into smaller pages so the server and client only handle a manageable chunk at a time. is missing. Response design is where API usability lives. It's the difference between an API that clients can rely on and one they have to defensively work around.
Consistent response structure
The most important rule is consistency. If your /users endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. returns { data: [...] } and your /orders endpoint returns [...] directly, every client has to special-case every endpoint. Pick a structure and use it everywhere.
Single resource
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}Single resources can be returned directly, no wrapper needed, because you're not going to add paginationWhat is pagination?Splitting a large set of results into smaller pages so the server and client only handle a manageable chunk at a time. metadata to a single item.
Collection response
Always wrap collections in an object, even if it feels like extra work up front:
{
"data": [
{ "id": 123, "name": "John Doe", "email": "john@example.com" },
{ "id": 124, "name": "Jane Smith", "email": "jane@example.com" }
],
"meta": {
"pagination": {
"page": 1,
"limit": 20,
"total_pages": 5,
"total_count": 100
}
}
}The reason for the wrapper: if you return a raw array and later need to add a meta field, you've broken every client that expected an array. The wrapper gives you a stable envelope you can extend without breaking changes.
PaginationWhat is pagination?Splitting a large set of results into smaller pages so the server and client only handle a manageable chunk at a time. strategies
Any endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users. that can return more than a few records needs pagination. Don't let clients download your entire database.
Offset-based pagination
Simple, and clients can jump to any page:
{
"data": [...],
"meta": {
"pagination": {
"page": 2,
"limit": 25,
"total_pages": 10,
"total_count": 250
},
"links": {
"first": "/users?page=1&limit=25",
"prev": "/users?page=1&limit=25",
"next": "/users?page=3&limit=25",
"last": "/users?page=10&limit=25"
}
}
}The downside: SELECT * FROM users LIMIT 25 OFFSET 10000 is slow on large tables because the database scans and discards the first 10,000 rows.
Cursor-based paginationWhat is cursor-based pagination?A pagination method that uses an indexed column value from the last seen row rather than OFFSET, staying fast on very large tables.
Better for large datasets and real-time data:
{
"data": [...],
"meta": {
"pagination": {
"has_more": true,
"next_cursor": "eyJpZCI6MTIzfQ=="
}
}
}The cursor encodes the position in the dataset (usually the ID or timestamp of the last item). The next request sends ?cursor=eyJpZCI6MTIzfQ== to get the next page. Fast, consistent, but you can't jump to page 7 directly.
Error response format
Plain text error messages break client code. JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. errors that vary in structure are almost as bad. Pick a shape and use it everywhere:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request validation failed",
"details": [
{
"field": "email",
"message": "Email is required"
},
{
"field": "password",
"message": "Password must be at least 8 characters"
}
],
"request_id": "req-123456789"
}
}The code field is a machine-readable string your client can switch on. The message is human-readable. details gives field-level validation errors. request_id lets you correlate the error to server logs.
Naming conventions
Pick one convention and stick to it. The two common choices for JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. APIs:
// snake_case - common in Python, Ruby, many public APIs
{
"first_name": "John",
"created_at": "2024-01-15"
}
// camelCase - common in JavaScript-heavy stacks
{
"firstName": "John",
"createdAt": "2024-01-15"
}Never mix them. { "firstName": "John", "created_at": "2024-01-15" } is the worst possible outcome.
Timestamps and null fields
Always use ISO 8601What is iso 8601?An international date and time format standard (e.g., 2024-03-15T10:30:00Z) used in APIs for unambiguous timestamps. format in UTC for timestamps. Never use Unix timestamps in responses, they're harder to read and debug:
{
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T14:45:30Z"
}For null fields, be consistent: either always include them as null, or always omit them. The safer choice is to include them as null for fields that could have values, omitting them forces clients to check for both undefined and null.
Embedding relationships
When one resource references another, you have two choices:
// Embedded - include the related object inline
{
"id": 123,
"user": {
"id": 456,
"name": "John Doe"
}
}
// Referenced - include the ID and let clients fetch if needed
{
"id": 123,
"user_id": 456
}Embed when the related data is almost always needed with the parent. Reference when it's only sometimes needed, or when it would create circular dependencies. Some APIs let the client choose: GET /orders/123?include=user,items.
Quick reference
| Scenario | Status code | Response body |
|---|---|---|
| GET resource | 200 | Resource object |
| GET collection | 200 | { data: [...], meta: {...} } |
| POST (created) | 201 | Created resource + Location header |
| PUT / PATCH | 200 | Updated resource |
| DELETE | 204 | Empty |
| Not found | 404 | { error: { code, message } } |
| Validation failed | 422 | { error: { code, message, details } } |
| Server error | 500 | { error: { code, message, request_id } } |