Learn/Practical Guides

Structuring JSON API Responses — Patterns That Scale

A well-designed API response format is the foundation of a great developer experience. This guide covers the patterns used by Stripe, GitHub, and Twilio — and shows you how to apply them to your own APIs.

Intermediate~16 min read

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.

Envelope Structure

Success Response

GET /api/users/42 — 200 OKjson
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": 45
12 },
13 "error": null
14}

Error Response

POST /api/users — 422 Unprocessable Entityjson
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": 15
16 }
17 ]
18 },
19 "meta": {
20 "request_id": "req_def456"
21 }
22}

Pagination Patterns

Offset-Based (Simple)

GET /api/posts?page=2&limit=20json
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": true
14 }
15 }
16}

Offset Pitfall

Offset pagination breaks when items are inserted or deleted between requests. Page 2 might skip or duplicate items if page 1's data changed.

Cursor-Based (Scalable)

GET /api/posts?after=eyJpZCI6NDJ9&limit=20json
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}
FactorOffsetCursor
Jump to page N
Real-time safe
Large datasetsSlow (OFFSET N)Fast (WHERE id > cursor)
ImplementationSimpleModerate
Total countEasyExpensive

Error Response Best Practices

A good error response tells the consumer what went wrong, where, and how to fix it.

Comprehensive error shapejson
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 StatusError CodeWhen to Use
400BAD_REQUESTMalformed JSON, missing Content-Type
401UNAUTHORIZEDMissing or invalid authentication
403FORBIDDENAuthenticated but insufficient permissions
404NOT_FOUNDResource does not exist
409CONFLICTDuplicate entry, optimistic lock failure
422VALIDATION_ERRORValid JSON but failed business rules
429RATE_LIMITEDToo many requests
500INTERNAL_ERRORUnexpected server error (hide details)

Real-World Patterns

List with Filtering Metadata

Search results with facetsjson
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": 10
14 }
15 },
16 "pagination": { "page": 1, "limit": 20, "has_more": true }
17 }
18}

Batch Response

POST /api/users/batch — mixed resultsjson
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": 1
19 }
20}

Response Design Checklist

Use a consistent envelope (data / error / meta)
Include a request_id in every response
Use appropriate HTTP status codes
Machine-readable error codes (strings, not numbers)
Per-field validation errors in a details array
Cursor pagination for large collections
Consistent date format (ISO 8601 with timezone)
Consistent naming convention (camelCase or snake_case)
Version your API from day one
Document error codes with examples

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

Frequently Asked Questions

Should I wrap API responses in an envelope?
For public APIs: yes. An envelope with "data", "error", and "meta" keys provides a consistent contract. For internal microservices where you control both sides, direct responses can be simpler.
How should I structure API error responses?
Include: HTTP status code, a machine-readable error code (string), a human-readable message, and optionally a details array with per-field validation errors. Always return errors in the same shape.
What is cursor-based pagination?
Instead of page numbers, cursor pagination uses an opaque token (usually an encoded ID) to mark the last item seen. The client passes this cursor to get the next page. It is more efficient for large datasets and handles real-time inserts gracefully.
Should I use camelCase or snake_case in JSON APIs?
Both are valid. JavaScript/TypeScript ecosystems prefer camelCase. Python/Ruby ecosystems use snake_case. The most important rule: be consistent within your API.
How do I version a JSON API?
Three common approaches: URL path versioning (/v2/users), header versioning (Accept: application/vnd.api+json;version=2), or query parameter versioning (?version=2). URL path is the most common and explicit.