Why Response Structure Matters
When your API returns inconsistent shapes, every consumer has to write defensive code. A consistent response contract means:
Predictable Parsing
Clients always know where to find data, errors, and metadata
Better DX
New developers can onboard faster when the pattern is obvious
Type Safety
TypeScript/Zod types can model a consistent envelope
Easy Debugging
Errors are always in the same place, with the same shape
The Envelope Pattern
An envelope wraps every response in a consistent outer object. This is the most widely used pattern in production APIs.
Success Response
1{2 "data": {3 "id": 42,4 "name": "Alice Chen",5 "email": "[email protected]",6 "role": "admin",7 "created_at": "2026-01-15T08:30:00Z"8 },9 "meta": {10 "request_id": "req_abc123",11 "response_time_ms": 4512 },13 "error": null14}Error Response
1{2 "data": null,3 "error": {4 "code": "VALIDATION_ERROR",5 "message": "Request body failed validation",6 "details": [7 {8 "field": "email",9 "message": "Must be a valid email address",10 "received": "not-an-email"11 },12 {13 "field": "age",14 "message": "Must be at least 18",15 "received": 1516 }17 ]18 },19 "meta": {20 "request_id": "req_def456"21 }22}Pagination Patterns
Offset-Based (Simple)
1{2 "data": [3 { "id": 21, "title": "Post 21" },4 { "id": 22, "title": "Post 22" }5 ],6 "meta": {7 "pagination": {8 "page": 2,9 "limit": 20,10 "total_items": 150,11 "total_pages": 8,12 "has_next": true,13 "has_prev": true14 }15 }16}Offset Pitfall
Cursor-Based (Scalable)
1{2 "data": [3 { "id": 43, "title": "Post 43" },4 { "id": 44, "title": "Post 44" }5 ],6 "meta": {7 "pagination": {8 "limit": 20,9 "has_more": true,10 "next_cursor": "eyJpZCI6NjJ9"11 }12 }13}| Factor | Offset | Cursor |
|---|---|---|
| Jump to page N | ✓ | ✗ |
| Real-time safe | ✗ | ✓ |
| Large datasets | Slow (OFFSET N) | Fast (WHERE id > cursor) |
| Implementation | Simple | Moderate |
| Total count | Easy | Expensive |
Error Response Best Practices
A good error response tells the consumer what went wrong, where, and how to fix it.
1{2 "error": {3 "code": "RESOURCE_NOT_FOUND",4 "message": "The requested user was not found",5 "details": [],6 "doc_url": "https://docs.example.com/errors/RESOURCE_NOT_FOUND",7 "request_id": "req_abc123"8 }9}| HTTP Status | Error Code | When to Use |
|---|---|---|
| 400 | BAD_REQUEST | Malformed JSON, missing Content-Type |
| 401 | UNAUTHORIZED | Missing or invalid authentication |
| 403 | FORBIDDEN | Authenticated but insufficient permissions |
| 404 | NOT_FOUND | Resource does not exist |
| 409 | CONFLICT | Duplicate entry, optimistic lock failure |
| 422 | VALIDATION_ERROR | Valid JSON but failed business rules |
| 429 | RATE_LIMITED | Too many requests |
| 500 | INTERNAL_ERROR | Unexpected server error (hide details) |
Real-World Patterns
List with Filtering Metadata
1{2 "data": [3 { "id": 1, "name": "React", "category": "frontend" },4 { "id": 2, "name": "Express", "category": "backend" }5 ],6 "meta": {7 "query": "framework",8 "total_results": 42,9 "facets": {10 "category": {11 "frontend": 18,12 "backend": 14,13 "fullstack": 1014 }15 },16 "pagination": { "page": 1, "limit": 20, "has_more": true }17 }18}Batch Response
1{2 "data": {3 "succeeded": [4 { "id": 1, "name": "Alice", "status": "created" },5 { "id": 2, "name": "Bob", "status": "created" }6 ],7 "failed": [8 {9 "index": 2,10 "input": { "name": "", "email": "bad" },11 "error": { "code": "VALIDATION_ERROR", "message": "Name is required" }12 }13 ]14 },15 "meta": {16 "total": 3,17 "succeeded": 2,18 "failed": 119 }20}Try These Tools
Response Design Checklist
Try It Yourself
Design an API response for a GET /api/products endpoint that returns a paginated list with cursor-based navigation.
Try It Yourself
Build a complete API response with envelope, pagination, and metadata