Working with JSON in REST APIs: The Complete Guide
JSON and REST APIs go hand in hand. Since Roy Fielding first described the REST architectural style, JSON has become the dominant format for data exchange in web APIs, overtaking XML by a wide margin. Today, the vast majority of public and internal APIs accept and return JSON.
Yet despite JSON's apparent simplicity, working with it in REST APIs involves more nuance than most tutorials cover. Content-Type headers, proper error responses, pagination patterns, status codes, nested relationships, date handling — each of these areas has conventions and pitfalls that affect how robust your API integrations will be.
This guide covers everything you need to know about working with JSON in REST APIs, from basic request and response patterns to advanced topics like error handling, versioning, and performance optimization. Whether you are building APIs or consuming them, you will find practical guidance and code examples you can apply immediately.
What Is a REST API and Why JSON?
A REST (Representational State Transfer) API is an interface that allows clients and servers to communicate over HTTP using standard methods like GET, POST, PUT, PATCH, and DELETE. Each resource (a user, a product, an order) is identified by a URL, and clients interact with resources by sending HTTP requests and receiving responses.
JSON became the standard data format for REST APIs because of several advantages:
- Human-readable — developers can inspect and debug payloads without special tools.
- Lightweight — JSON has minimal syntax overhead compared to XML, resulting in smaller payloads.
- Native to JavaScript — browsers parse JSON natively with
JSON.parse(), making it ideal for web clients. - Universal support — every major programming language has built-in or standard-library JSON support.
- Flexible schema — JSON does not require a predefined schema (unlike XML with XSD), making it easier to evolve APIs over time.
HTTP Headers: Content-Type and Accept
The single most important detail when working with JSON APIs is getting the HTTP headers right. Two headers govern how JSON is sent and received:
Content-Type
The Content-Type header tells the server what format the request body is in. For JSON, the correct value is:
Content-Type: application/jsonOmitting this header or setting it incorrectly is one of the most common causes of "400 Bad Request" errors. Many APIs will reject the request or try to parse the body as form data, leading to confusing errors.
Accept
The Accept header tells the server what format the client wants in the response. For JSON:
Accept: application/jsonWhile many APIs default to JSON, explicitly setting the Accept header is a best practice. Some APIs support multiple formats (JSON, XML, CSV) and use content negotiation to decide which to return.
Sending JSON in API Requests
Let us look at how to properly send JSON data in the most common HTTP methods.
POST: Creating a Resource
POST requests create new resources. The JSON payload describes the resource to create:
// Creating a new user with fetch
const response = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
},
body: JSON.stringify({
name: 'Alice Johnson',
email: 'alice@example.com',
role: 'editor'
})
});
const newUser = await response.json();
console.log(newUser.id); // 42 (server-assigned ID)Notice that we call JSON.stringify() on the request body. The fetch API does not automatically serialize objects to JSON — you must do this explicitly. Forgetting to stringify is a common source of bugs.
PUT vs. PATCH: Updating a Resource
Both PUT and PATCH update existing resources, but they differ in semantics:
- PUT replaces the entire resource. You must send all fields, even ones that have not changed.
- PATCH applies a partial update. You only send the fields you want to change.
// PUT — full replacement (must include all fields)
await fetch('https://api.example.com/users/42', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: 'Alice Johnson',
email: 'alice.johnson@newdomain.com', // changed
role: 'editor' // unchanged but required
})
});
// PATCH — partial update (only changed fields)
await fetch('https://api.example.com/users/42', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'alice.johnson@newdomain.com' // only the field that changed
})
});PATCH is generally preferred in modern APIs because it reduces payload size and avoids accidentally overwriting fields with stale data.
GET: Retrieving Resources
GET requests do not have a request body. Any parameters go in the URL as query strings:
// Fetching a list with query parameters
const params = new URLSearchParams({
status: 'active',
sort: 'created_at',
order: 'desc',
limit: '20',
offset: '0'
});
const response = await fetch(
`https://api.example.com/users?${params}`,
{
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
}
}
);
const data = await response.json();
console.log(data.users.length); // 20Parsing JSON API Responses
When the server responds, you need to parse the JSON body and handle the various response scenarios correctly.
Checking the Status Code First
Always check the HTTP status code before parsing the response body. A common mistake is calling .json() on every response without checking whether the request actually succeeded:
async function apiRequest(url, options = {}) {
const response = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
...options.headers
}
});
// Parse the JSON body (both success and error responses)
let body;
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
body = await response.json();
} else {
body = await response.text();
}
// Throw on error status codes
if (!response.ok) {
const error = new Error(
body?.message || `HTTP ${response.status}: ${response.statusText}`
);
error.status = response.status;
error.body = body;
throw error;
}
return body;
}
// Usage
try {
const user = await apiRequest('https://api.example.com/users/42');
console.log(user.name);
} catch (err) {
if (err.status === 404) {
console.log('User not found');
} else if (err.status === 401) {
console.log('Authentication required');
} else {
console.error('API error:', err.message);
}
}This wrapper function handles three important details: it sets the correct headers, it checks the Content-Type before parsing, and it throws a structured error for non-2xx responses. Most production applications benefit from a similar utility function.
Designing JSON API Response Structures
Well-designed JSON responses follow consistent patterns that make the API predictable and easy to consume. Here are the most common conventions.
Single Resource
{
"id": 42,
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "editor",
"created_at": "2026-01-15T10:30:00Z",
"updated_at": "2026-02-20T14:22:00Z"
}Collection with Pagination
{
"data": [
{ "id": 1, "name": "Alice Johnson", "role": "editor" },
{ "id": 2, "name": "Bob Smith", "role": "viewer" },
{ "id": 3, "name": "Carol White", "role": "admin" }
],
"pagination": {
"total": 156,
"limit": 20,
"offset": 0,
"has_more": true
},
"links": {
"self": "/users?limit=20&offset=0",
"next": "/users?limit=20&offset=20",
"last": "/users?limit=20&offset=140"
}
}Wrapping collections in a data property (rather than returning a bare array) allows you to include pagination metadata and links alongside the results. This is the approach recommended by the JSON:API specification and used by most well-designed APIs.
Error Response
{
"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": "role",
"message": "Must be one of: admin, editor, viewer.",
"value": "superuser"
}
]
}
}Good error responses include a machine-readable error code, a human-readable message, and details about specific field-level errors when applicable. This structure enables clients to display targeted error messages to users rather than generic "Something went wrong" alerts.
Error Handling Patterns
Robust error handling is what separates production-quality API integrations from tutorial-level code. Here are the patterns you should implement.
HTTP Status Codes
REST APIs communicate outcomes through standard HTTP status codes. The most important ones for JSON APIs:
200 OK— Request succeeded. Response body contains the result.201 Created— Resource created successfully. Response body contains the new resource.204 No Content— Request succeeded with no response body (common for DELETE).400 Bad Request— Invalid JSON syntax or invalid field values.401 Unauthorized— Missing or invalid authentication credentials.403 Forbidden— Authenticated but insufficient permissions.404 Not Found— The requested resource does not exist.409 Conflict— The request conflicts with the current state (e.g., duplicate email).422 Unprocessable Entity— Valid JSON, but semantically invalid data.429 Too Many Requests— Rate limit exceeded. Check theRetry-Afterheader.500 Internal Server Error— Server-side error. The client should retry or report the issue.
Retry Logic with Exponential Backoff
For transient errors (429, 500, 502, 503, 504), implement retry logic with exponential backoff:
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
// Don't retry client errors (except 429)
if (response.status >= 400 && response.status < 500
&& response.status !== 429) {
return response;
}
if (response.ok) return response;
// Retry on server errors and rate limiting
if (attempt < maxRetries) {
const retryAfter = response.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter) * 1000
: Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(r => setTimeout(r, delay));
continue;
}
return response;
} catch (networkError) {
// Retry on network failures
if (attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await new Promise(r => setTimeout(r, delay));
continue;
}
throw networkError;
}
}
}Handling Dates and Times in JSON
JSON has no native date type, which means dates are transmitted as strings. The universally accepted format is ISO 8601:
{
"event": "User signed up",
"timestamp": "2026-03-01T14:30:00Z",
"local_time": "2026-03-01T09:30:00-05:00"
}Always use ISO 8601 format with timezone information. The trailing Z indicates UTC. The -05:00 variant indicates a UTC offset. Avoid sending dates as Unix timestamps in user-facing APIs — while they are unambiguous, they are not human-readable when you inspect the JSON.
On the client side, parse ISO 8601 dates carefully:
// Parse ISO 8601 from JSON response
const data = await response.json();
const signupDate = new Date(data.timestamp);
// Format for display (respects user's locale)
const formatted = signupDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
});
// "March 1, 2026, 02:30 PM UTC"Nested Resources and Relationships
APIs often need to represent relationships between resources. There are two main approaches:
Embedding (Eager Loading)
{
"id": 42,
"title": "Quarterly Report",
"author": {
"id": 7,
"name": "Alice Johnson",
"avatar_url": "https://cdn.example.com/avatars/7.jpg"
},
"comments": [
{
"id": 101,
"text": "Great analysis!",
"author": { "id": 3, "name": "Bob Smith" },
"created_at": "2026-02-28T16:00:00Z"
}
]
}Embedding related resources directly in the response reduces the number of API calls the client needs to make. This is ideal when the client almost always needs the related data.
References (Lazy Loading)
{
"id": 42,
"title": "Quarterly Report",
"author_id": 7,
"comment_ids": [101, 102, 103],
"links": {
"author": "/users/7",
"comments": "/reports/42/comments"
}
}References keep the initial response small and let the client fetch related resources only when needed. This is better when related data is large or rarely used.
Many APIs offer both approaches through query parameters like ?include=author,comments or ?expand=author, letting the client decide what to load.
Common Mistakes When Working with JSON APIs
1. Not Setting Content-Type Headers
The number one mistake is forgetting to set Content-Type: application/json. Without it, the server may parse your body as URL-encoded form data, leading to a 400 error or silently incorrect data. Every POST, PUT, and PATCH request with a JSON body must include this header.
2. Sending Strings Instead of JSON
Accidentally sending a stringified-stringified body is more common than you might think:
// WRONG: double-stringified
const body = JSON.stringify(JSON.stringify({ name: 'Alice' }));
// Result: '"{\"name\":\"Alice\"}"' (a JSON string containing escaped JSON)
// CORRECT: single stringify
const body = JSON.stringify({ name: 'Alice' });
// Result: '{"name":"Alice"}'If your API is receiving string values where you expect objects, check whether you are double-stringifying the request body. Using our JSON Formatter to inspect the payload can reveal this immediately.
3. Ignoring Error Response Bodies
Many developers check for error status codes but do not read the error response body. Well-designed APIs return detailed error information in JSON format that tells you exactly what went wrong and how to fix it. Always parse the error body — it is the API trying to help you.
4. Assuming Field Order Matters
JSON objects are unordered by specification. {"a":1,"b":2} and {"b":2,"a":1} are semantically identical. Never write code that depends on JSON keys appearing in a particular order. If your API needs ordered data, use an array.
5. Not Handling Null and Missing Fields
In JSON, there is a difference between a field set to null and a field that is absent entirely. Your code should handle both cases:
// Safe field access with defaults
const userName = response.user?.name ?? 'Unknown';
const userAge = response.user?.age ?? null; // null if missing or nullBest Practices for JSON in REST APIs
- Always set Content-Type and Accept headers. This is non-negotiable for reliable API communication.
- Use consistent naming conventions. Pick
camelCaseorsnake_caseand use it everywhere. Most JSON APIs use snake_case for field names. - Return appropriate status codes. Do not return 200 with an error message in the body. Use 4xx and 5xx codes so clients can handle errors programmatically.
- Include pagination for collections. Never return unbounded collections. Always provide limit/offset or cursor-based pagination.
- Use ISO 8601 for dates. Avoid Unix timestamps, custom formats, or locale-specific date strings.
- Version your API. Use URL versioning (
/v1/users) or header versioning to allow non-breaking changes. - Validate request bodies. Reject malformed JSON early with clear 400-level error messages.
- Use a JSON viewer for debugging. When API responses are complex, paste them into a JSON Viewer to explore the structure interactively.
Debugging JSON API Issues
When an API integration is not working, the debugging process follows a predictable pattern:
- Inspect the raw request and response. Use your browser's DevTools Network tab or a tool like Postman to see the exact headers and body being sent and received.
- Validate the JSON. Paste the request body into the JSON Validator to confirm it is valid JSON.
- Format the response. Copy the raw response and paste it into the JSON Formatter to make nested structures visible.
- Check headers. Verify that Content-Type is
application/jsonand nottext/plainorapplication/x-www-form-urlencoded. - Read the error body. The server's error response usually tells you exactly what is wrong. Do not ignore it.
Debug Your API Responses Now
Working with JSON APIs is much easier when you have the right tools. Use our JSON Formatter to pretty-print API responses for inspection, the JSON Validator to catch syntax errors in request bodies, and the JSON Viewer to explore deeply nested response structures interactively.
All tools run entirely in your browser — your API data stays private and never leaves your machine.
Conclusion
JSON and REST APIs are foundational technologies for modern web development. Getting the basics right — proper headers, consistent response structures, robust error handling, and thoughtful date formatting — makes the difference between fragile integrations that break at the first edge case and resilient ones that handle real-world complexity gracefully.
The patterns in this guide apply whether you are building APIs or consuming them. Set your headers correctly, validate your payloads, handle errors gracefully, and use the right tools for debugging. REST APIs are simple in concept but demand attention to detail in practice — and JSON is the format that makes it all work.