API Response Best Practices: Structure, Error Handling, and Pagination
A practical guide to designing consistent, developer-friendly API responses — covering data structure, error formats, pagination strategies, and versioning.
Designing a good API is about more than choosing the right endpoints and HTTP methods. The structure of your responses — how you wrap data, report errors, handle pagination, and version your contracts — has a direct impact on how easily other developers can integrate with your API. Inconsistent or poorly structured responses create friction, generate support tickets, and drive developers to competing APIs. This guide covers the patterns and practices that make API responses predictable, useful, and easy to consume.
The Foundation: Consistency
The single most important principle is consistency. Every endpoint in your API should return responses that follow the same structure, use the same naming conventions, and handle errors the same way. Developers who integrate with one endpoint should be able to predict the shape of responses from every other endpoint without reading additional documentation.
Structuring Successful Responses
Wrap Data in a Consistent Envelope
A common pattern is to wrap response data in a top-level object with a predictable key:
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2026-01-15T09:30:00Z"
}
}
For list endpoints, the data field contains an array:
{
"data": [
{ "id": "1", "name": "Alice" },
{ "id": "2", "name": "Bob" }
],
"meta": {
"total": 42,
"page": 1,
"perPage": 20,
"totalPages": 3
}
}
The envelope pattern has clear benefits: there is always a predictable top-level structure, metadata (pagination, rate limits) lives alongside the data, and adding new fields to the envelope does not break existing clients that only read data.
Alternative: Direct Response
Some APIs return data directly without an envelope:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice",
"email": "alice@example.com"
}
This is simpler and reduces payload size. Stripe, GitHub, and many others use this approach. The trade-off is that metadata (pagination info, rate limit status) must be conveyed through HTTP headers rather than the response body.
Both approaches are valid. Pick one and use it everywhere.
Naming Conventions
Use consistent naming across all responses:
- Use camelCase (JavaScript/JSON convention) or snake_case (Python/Ruby convention) consistently. Do not mix them.
- Use ISO 8601 timestamps for all date/time fields:
"2026-06-29T14:30:00Z". Never use Unix timestamps as numbers — they are ambiguous (seconds vs. milliseconds) and not human-readable. - Use consistent boolean naming:
isActive,isDeleted,hasPermission— prefixes that make the boolean nature obvious. - Use plural nouns for collections:
users,orders,items— neveruserListoruserData.
Include HATEOAS Links (When Appropriate)
For APIs that benefit from discoverability, include navigation links in responses:
{
"data": { "id": "42", "name": "Alice" },
"links": {
"self": "/api/users/42",
"orders": "/api/users/42/orders",
"avatar": "/api/users/42/avatar"
}
}
HATEOAS (Hypermedia as the Engine of Application State) is a core REST principle, but it adds complexity. It is most valuable for large, public APIs where discoverability reduces documentation burden. For internal APIs with a small number of consumers, it may be overkill.
Error Handling
Error responses are where most APIs fall short. A well-designed error response tells the developer exactly what went wrong and what to do about it.
Use Appropriate HTTP Status Codes
Do not return 200 OK for everything with an error flag in the body. Use HTTP status codes correctly:
- 400 Bad Request — the request is malformed or contains invalid parameters
- 401 Unauthorized — authentication is missing or invalid
- 403 Forbidden — authenticated but not authorized for this action
- 404 Not Found — the requested resource does not exist
- 409 Conflict — the request conflicts with existing state (duplicate entry, version mismatch)
- 422 Unprocessable Entity — the request is well-formed but contains semantic errors (validation failures)
- 429 Too Many Requests — rate limit exceeded
- 500 Internal Server Error — unexpected server failure
- 503 Service Unavailable — temporary overload or maintenance
Structured Error Response
Every error response should follow the same structure:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid fields.",
"details": [
{
"field": "email",
"message": "Must be a valid email address.",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be a positive integer.",
"value": -5
}
]
}
}
Key elements:
- Machine-readable error code (
VALIDATION_ERROR,RESOURCE_NOT_FOUND,RATE_LIMIT_EXCEEDED). Clients can branch on this without parsing human-readable messages. - Human-readable message explaining the error at a high level.
- Details array for validation errors, listing each invalid field, what is wrong, and optionally the offending value.
- Request ID for debugging:
"requestId": "req_abc123". When a developer contacts support, they can provide this ID and you can trace the exact request in your logs.
Never Expose Internal Details
Error messages should never leak stack traces, database queries, internal file paths, or library names. These details are a security risk and provide no value to the API consumer. Log them server-side; return clean, generic messages to the client.
Pagination
When an endpoint returns a collection, you need pagination. Unpaginated list endpoints are a performance and usability problem waiting to happen.
Offset-Based Pagination
The simplest approach. The client specifies a page number and page size (or an offset and limit):
GET /api/users?page=2&perPage=20
Response:
{
"data": [ ... ],
"meta": {
"total": 150,
"page": 2,
"perPage": 20,
"totalPages": 8
}
}
Pros: Simple to understand, easy to implement, allows random access to any page.
Cons: Performance degrades on large datasets (the database must count and skip rows). Results can be inconsistent if data changes between page requests (items added or deleted shift the offsets).
Cursor-Based Pagination
Uses a pointer (cursor) to the last item seen. The client passes the cursor to get the next page:
GET /api/users?cursor=eyJpZCI6MTAwfQ&limit=20
Response:
{
"data": [ ... ],
"meta": {
"nextCursor": "eyJpZCI6MTIwfQ",
"hasMore": true
}
}
The cursor is typically a Base64-encoded value containing the sort key (ID, timestamp) of the last item.
Pros: Consistent performance regardless of dataset size. Stable results even when data changes between requests. Better for real-time feeds and infinite scroll UIs.
Cons: Cannot jump to an arbitrary page. More complex to implement. Clients must track the cursor.
Which to Choose
- Offset pagination for admin dashboards, search results, and any UI that shows numbered page controls.
- Cursor pagination for feeds, timelines, infinite scroll, and any dataset large enough that offset performance is a concern.
Pagination Best Practices
- Always paginate. Even if the dataset is small today, design for growth. Set a sensible default page size (20-50) and a maximum (100-200).
- Include total count (for offset pagination) so clients can show progress indicators.
- Return the pagination metadata in every response, even when there is only one page. This lets clients write uniform code.
- Use Link headers as an alternative or complement to body metadata:
Link: </api/users?page=2>; rel="next", </api/users?page=8>; rel="last"
Versioning
APIs change. When they do, you need a versioning strategy that lets existing clients continue working while new clients adopt the updated contract.
URL Path Versioning
GET /api/v1/users
GET /api/v2/users
The most visible and widely used approach. Easy to understand, easy to route, easy to document. Used by GitHub, Stripe, and most public APIs.
Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+json
Cleaner URLs but less discoverable. Used by GitHub (as an alternative to path versioning) and some enterprise APIs.
Versioning Best Practices
- Start at v1. Do not skip the version number — an unversioned API is a versioning debt waiting to accumulate.
- Support at most two versions simultaneously. Deprecate old versions with a clear timeline (6-12 months notice).
- Include deprecation headers in responses from old versions:
Sunset: Sat, 01 Jan 2028 00:00:00 GMT Deprecation: true - Never make breaking changes within a version. Adding a new field is safe. Removing a field, changing a type, or renaming a key requires a new version.
Rate Limiting
Communicate rate limit status in response headers so clients can self-regulate:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1624982400
When the limit is exceeded, return 429 with a Retry-After header:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content Types and Encoding
- Always return
Content-Type: application/jsonfor JSON responses. - Support
Accept-Encoding: gzipfor compression. - Use
charset=utf-8in the Content-Type header. - Return
Content-Languagewhen responses are localized.
Putting It All Together
A well-designed API response for a paginated list of users might look like this:
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Alice",
"email": "alice@example.com",
"isActive": true,
"createdAt": "2026-01-15T09:30:00Z"
}
],
"meta": {
"total": 150,
"page": 1,
"perPage": 20,
"totalPages": 8
}
}
And an error response:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid fields.",
"details": [
{
"field": "email",
"message": "Must be a valid email address."
}
],
"requestId": "req_7f3a9b2c"
}
}
Both follow predictable structures that clients can parse uniformly.
Conclusion
Good API response design is an investment in developer experience. Consistent envelopes, clear error messages, thoughtful pagination, and responsible versioning reduce integration time, decrease support burden, and make your API a pleasure to work with. The patterns described here are not revolutionary — they are the established conventions used by the most respected APIs in the industry. Adopt them early, enforce them with linting and code review, and your API consumers will thank you.