REST API JSON Best Practices
Designing consistent, predictable JSON responses is one of the most impactful things you can do for your API’s usability. This guide covers the conventions used by top APIs like Stripe, GitHub, and Twilio — with code examples you can adopt immediately.
Naming Conventions
Choose one naming convention and use it consistently across all endpoints:
| Convention | Example | Used By |
|---|---|---|
| camelCase | firstName, createdAt | JavaScript, Stripe |
| snake_case | first_name, created_at | Python, GitHub, Twitter |
| kebab-case | first-name | Rare in JSON (used in URLs) |
Convert between conventions instantly with our String Case Converter.
Rule: Match the naming convention of your primary consumer. If your API is consumed mostly by JavaScript frontends, use camelCase. If by Python backends, use snake_case.
Common Naming Mistakes to Avoid
- Mixing conventions — Using
firstNamein one endpoint andfirst_namein another creates confusion and bugs. - Abbreviations — Prefer
descriptionoverdesc, andconfigurationoverconfig. Explicit names reduce documentation dependency. - Boolean prefixes — Use
is,has, orcanprefixes for booleans:isActive,hasPermission,canEdit. - Pluralization — Use plural nouns for arrays:
itemsnotitem,addressesnotaddress.
Response Envelope Pattern
Wrap your responses in a consistent envelope:
{
"data": {
"id": "usr_123",
"name": "Alice Johnson",
"email": "alice@example.com"
},
"meta": {
"requestId": "req_abc123",
"timestamp": "2026-03-22T14:30:00Z"
}
}
This pattern separates the actual data from metadata and makes your API easier to extend without breaking changes.
When to Use Envelopes vs Flat Responses
| Approach | Pros | Cons | Best For |
|---|---|---|---|
Envelope (data wrapper) | Extensible, consistent, room for meta | Slightly verbose | Public APIs, multi-consumer APIs |
| Flat response | Simpler, less nesting | Hard to add metadata later | Internal microservices |
| JSON:API spec | Standardized, tooling available | Complex, steep learning curve | Large organizations |
Recommendation: Use envelopes for public APIs. The overhead is minimal, but the extensibility is invaluable when you need to add pagination metadata, deprecation notices, or rate limit information later.
Error Response Format
Use a consistent error format across all endpoints:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The request body contains invalid fields.",
"details": [
{ "field": "email", "message": "Must be a valid email address" },
{ "field": "age", "message": "Must be a positive integer" }
]
}
}
Rules:
- Always return an
errorobject (never a plain string). - Include a machine-readable
codefor programmatic handling. - Include a human-readable
messagefor debugging. - Use
detailsarray for field-level validation errors.
HTTP Status Codes for JSON APIs
Pair your JSON error responses with the correct HTTP status codes:
| Status | When to Use | Error Code Example |
|---|---|---|
400 | Malformed request body, invalid JSON | INVALID_REQUEST |
401 | Missing or expired authentication | AUTHENTICATION_REQUIRED |
403 | Authenticated but insufficient permissions | FORBIDDEN |
404 | Resource not found | RESOURCE_NOT_FOUND |
409 | Conflict (e.g., duplicate email) | CONFLICT |
422 | Valid JSON but fails business rules | VALIDATION_ERROR |
429 | Rate limit exceeded | RATE_LIMIT_EXCEEDED |
500 | Unexpected server error | INTERNAL_ERROR |
Anti-pattern: Never return 200 OK with an error in the body. This confuses HTTP clients, caches, and monitoring tools.
Pagination
For list endpoints, use cursor-based pagination:
{
"data": [
{ "id": "usr_001", "name": "Alice" },
{ "id": "usr_002", "name": "Bob" }
],
"pagination": {
"hasMore": true,
"nextCursor": "usr_002",
"totalCount": 150
}
}
Cursor-based pagination is more reliable than offset-based (?page=2) because it handles insertions and deletions without skipping or duplicating records.
Pagination Comparison
| Method | Consistency | Performance | Use Case |
|---|---|---|---|
| Cursor-based | ✅ Stable during mutations | ✅ O(1) seek | Real-time feeds, large datasets |
| Offset-based | ❌ Skips/duplicates on mutations | ❌ O(n) skip | Simple admin panels, small datasets |
| Keyset-based | ✅ Stable | ✅ Uses index | Sorted, immutable data |
Implementation tip: Use btoa(JSON.stringify({id, createdAt})) to create opaque cursors that encode the sort key without exposing your database internals.
Versioning Your API
Choose a versioning strategy and apply it consistently:
# URL path versioning (most common)
GET /v1/users
GET /v2/users
# Header versioning
GET /users
Accept: application/vnd.myapi.v2+json
# Query parameter versioning
GET /users?version=2
Recommendation: URL path versioning (/v1/, /v2/) is the most explicit and easiest to understand. Header versioning is cleaner but harder to test in a browser.
Deprecation Strategy
When deprecating an API version, communicate clearly:
{
"data": { "id": "usr_123" },
"meta": {
"deprecation": {
"message": "API v1 will be removed on 2027-01-01. Migrate to v2.",
"sunsetDate": "2027-01-01",
"migrationGuide": "https://docs.example.com/migrate-v1-to-v2"
}
}
}
Add the Sunset HTTP header as well: Sunset: Sat, 01 Jan 2027 00:00:00 GMT.
Dates and Timestamps
Always use ISO 8601 format with timezone:
{
"createdAt": "2026-03-22T14:30:00Z",
"updatedAt": "2026-03-22T15:45:30+03:00"
}
Convert timestamps between Unix epoch and human-readable dates with our Unix Timestamp Converter.
Rules:
- Use UTC (
Zsuffix) for server-generated timestamps. - Include timezone offset for user-facing times.
- Never use ambiguous formats like
03/22/2026(US) or22/03/2026(EU). - For date-only values (no time component), use
YYYY-MM-DDformat:"birthDate": "1990-05-15".
Null vs Absent Fields
Be explicit about null values:
{
"name": "Alice",
"avatar": null,
"bio": null
}
Rule: Include fields with null values rather than omitting them. This tells the consumer “this field exists but has no value” versus “this field doesn’t exist,” which are semantically different.
Partial Updates (PATCH)
For partial updates, only send the fields being modified. The server should interpret absent fields as “no change” and explicit null as “clear this field”:
// PATCH /users/usr_123
{
"bio": "Software developer",
"avatar": null
}
// Result: bio is updated, avatar is cleared, name is unchanged
Rate Limiting
Always include rate limit information in response headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1679500800
Retry-After: 30
When the limit is exceeded, return a helpful JSON response:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "You have exceeded the rate limit of 1000 requests per hour.",
"retryAfter": 30,
"limit": 1000,
"resetAt": "2026-03-22T15:00:00Z"
}
}
Performance Tips
- Minify production responses — Remove whitespace with
JSON.stringify(data)(no indentation). Use our JSON Formatter during development, minified in production. - Use pagination — Never return unbounded lists. Default to 20-50 items per page.
- Support field selection — Let consumers request only the fields they need:
?fields=id,name,email. This reduces payload size significantly for mobile clients. - Compress responses — Enable gzip/brotli compression at the server level. JSON compresses extremely well (often 80-90% reduction).
- Generate TypeScript types — Create client-side type safety from your API responses with our JSON to TypeScript Converter.
- Use ETags for caching — Return
ETagheaders and supportIf-None-Matchto enable304 Not Modifiedresponses for unchanged resources.
Response Size Optimization
| Technique | Typical Savings | Complexity |
|---|---|---|
| Gzip compression | 80-90% | Low (server config) |
| Field selection | 30-70% | Medium |
| Sparse fieldsets | 40-60% | Medium |
| Remove null fields | 5-15% | Low |
| Minified JSON | 10-20% | None (default) |
Security Considerations
- Never expose internal IDs — Use UUIDs or prefixed IDs (
usr_123) instead of sequential integers that reveal record counts. - Sanitize output — Escape HTML entities in JSON string values to prevent XSS when rendered in browsers.
- Limit response depth — Set a maximum nesting level to prevent circular reference attacks.
- Validate Content-Type — Reject requests without
Content-Type: application/jsonto prevent CSRF attacks. - Use HTTPS only — Never serve API responses over HTTP. Set
Strict-Transport-Securityheaders.
FAQ
Should I use JSON:API or GraphQL instead of REST?
REST with well-designed JSON responses covers 90% of use cases. Consider GraphQL when clients need flexible queries across many related resources (e.g., a mobile app showing user profiles with posts, comments, and followers in one screen). Consider JSON:API when you need a formal specification for large teams. For most projects, simple REST with consistent conventions is the best choice.
How do I handle API versioning for breaking changes?
A breaking change is anything that removes a field, changes a field’s type, or alters the behavior of an endpoint. For breaking changes, increment the major version (/v1/ → /v2/). For additive changes (new fields, new endpoints), keep the same version — these are backward-compatible.
What’s the maximum recommended JSON response size?
Keep individual responses under 1 MB for web clients and under 256 KB for mobile clients. For larger datasets, use pagination or streaming (NDJSON). If a single resource consistently exceeds these limits, consider splitting it into sub-resources with separate endpoints.
How should I handle file uploads in a JSON API?
Don’t embed large binary data in JSON. Use multipart/form-data for file uploads, or implement a two-step process: (1) Get a pre-signed URL from your API, (2) Upload directly to cloud storage (S3, GCS). Return the file URL in the JSON response after upload completes.
This article is part of our JSON Developer Guide series.