JSON API Response Format — Structuring REST API Responses
A consistent JSON response format makes APIs predictable and easier to consume. Here's how to structure JSON API responses — envelope vs. flat, error format, pagination, and...
Consistent API response structure reduces client-side complexity. Teams that define a response format once — for success, error, and pagination cases — write less defensive code everywhere that calls the API.
Use the JSON Diff Tool to compare API responses across versions.
Flat vs envelope response format
Flat (no envelope)
Return the resource directly:
GET /users/123
{
"id": 123,
"name": "Alice",
"email": "alice@example.com"
}
Simple, HTTP status code carries success/error state. Used by Stripe, GitHub APIs.
Envelope format
Wrap the resource in a consistent container:
GET /users/123
{
"data": {
"id": 123,
"name": "Alice",
"email": "alice@example.com"
},
"meta": {
"requestId": "abc-123",
"timestamp": "2026-05-11T10:00:00Z"
}
}
Adds room for metadata without breaking the data shape. Used by Google APIs, JSON:API standard.
Successful list response with pagination
GET /users?page=2&limit=20
{
"data": [
{ "id": 21, "name": "Charlie" },
{ "id": 22, "name": "Diana" }
],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
Cursor-based pagination (better for large datasets):
{
"data": [ ... ],
"pagination": {
"nextCursor": "eyJpZCI6IDIyfQ==",
"prevCursor": "eyJpZCI6IDIxfQ==",
"hasNext": true,
"limit": 20
}
}
Error response format
Consistent error format lets clients handle errors without parsing messages:
HTTP/1.1 400 Bad Request
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be a positive integer",
"value": -5
}
]
},
"requestId": "req_abc123"
}
Common error codes:
// 400 Bad Request:
{ "error": { "code": "VALIDATION_ERROR", "message": "..." } }
// 401 Unauthorized:
{ "error": { "code": "UNAUTHORIZED", "message": "Authentication required" } }
// 403 Forbidden:
{ "error": { "code": "FORBIDDEN", "message": "Insufficient permissions" } }
// 404 Not Found:
{ "error": { "code": "NOT_FOUND", "message": "User 123 not found" } }
// 409 Conflict:
{ "error": { "code": "CONFLICT", "message": "Email already registered" } }
// 429 Too Many Requests:
{ "error": { "code": "RATE_LIMITED", "message": "..." },
"retryAfter": 60 }
// 500 Internal Server Error:
{ "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred" } }
Implementing consistent responses in Express
// Response helpers:
const respond = {
success(res, data, statusCode = 200) {
res.status(statusCode).json({ data });
},
created(res, data) {
res.status(201).json({ data });
},
error(res, code, message, statusCode = 400, details = null) {
const body = { error: { code, message } };
if (details) body.error.details = details;
res.status(statusCode).json(body);
},
notFound(res, resource) {
res.status(404).json({
error: { code: 'NOT_FOUND', message: `${resource} not found` }
});
},
paginated(res, data, page, limit, total) {
res.json({
data,
pagination: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1,
},
});
},
};
// Usage:
app.get('/users/:id', async (req, res) => {
const user = await db.users.findById(req.params.id);
if (!user) return respond.notFound(res, 'User');
respond.success(res, user);
});
app.post('/users', async (req, res) => {
const { error, value } = userSchema.validate(req.body);
if (error) return respond.error(res, 'VALIDATION_ERROR', 'Validation failed', 400,
error.details.map(d => ({ field: d.path[0], message: d.message })));
const user = await db.users.create(value);
respond.created(res, user);
});
JSON:API specification
JSON:API is a formal standard for JSON API response format:
{
"data": {
"type": "articles",
"id": "1",
"attributes": {
"title": "Hello World",
"body": "Article content"
},
"relationships": {
"author": {
"data": { "type": "people", "id": "9" }
}
}
},
"included": [
{
"type": "people",
"id": "9",
"attributes": {
"name": "Alice"
}
}
]
}
JSON:API handles relationships, sparse fieldsets, and included resources in a standardized way — useful for complex APIs but adds verbosity.
Versioning in response format
Add a version field to enable future evolution:
{
"version": "2",
"data": { ... }
}
Or version via the URL (/v2/users) or Accept header (Accept: application/vnd.myapi.v2+json).
Related tools
- JSON Diff Tool — compare API responses between versions
- JSON Schema Validation — validate response structure
- HTTP Status Codes Reference — choosing the right status code
Related posts
- Comparing JSON Structurally (Not Just as Strings) — Two JSON documents can be byte-different and semantically identical. Or byte-ide…
- How to Compare JSON Objects — Deep Equality and Diff in JavaScript and Python — Comparing JSON objects with == won't work for deep equality. Here's how to deep-…
- Detecting Changes in JSON Data — Audit Logs, Diffs, and Change Tracking — Detecting what changed in a JSON document is essential for audit logs, versionin…
- JSON Diff Tool — Compare Two JSON Objects and Find Differences — A JSON diff tool compares two JSON structures semantically, not textually. It fi…
- JSON Schema Validation — Validate JSON Structure and Types — JSON Schema defines the structure and types your JSON must conform to. Here's ho…
Related tool
Compare two JSON objects structurally with field-by-field diff.
Written by Mian Ali Khalid. Part of the Data & Format pillar.