DEV Community

Muiz
Muiz

Posted on

JSON API Response Design: Small Decisions That Save Months of Maintenance

Most developers spend a lot of time designing databases, infrastructure, and application architecture.

But surprisingly little time is spent designing something every client consumes:

The API response format.

A poorly designed response structure creates confusion, duplicate code, inconsistent error handling, and unnecessary bugs across frontend, mobile, backend, and QA teams.

Let's explore a practical approach to JSON API response design.

The Problem

Many APIs start like this:

{
  "name": "John Doe",
  "email": "john@example.com"
}
Enter fullscreen mode Exit fullscreen mode

Seems fine.

Until an error occurs:

{
  "error": "User not found"
}
Enter fullscreen mode Exit fullscreen mode

Then another endpoint returns:

{
  "success": false,
  "message": "Invalid token"
}
Enter fullscreen mode Exit fullscreen mode

And another one returns:

{
  "status": "error",
  "detail": "Permission denied"
}
Enter fullscreen mode Exit fullscreen mode

Now every consumer needs special handling for every endpoint.

As the API grows, the problem grows with it.


A Consistent Response Envelope

A common pattern is using a standard wrapper around all responses.

Success:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "John Doe"
  }
}
Enter fullscreen mode Exit fullscreen mode

Failure:

{
  "success": false,
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found"
  }
}
Enter fullscreen mode Exit fullscreen mode

This gives clients a predictable structure regardless of the endpoint.


Include Machine-Readable Error Codes

Avoid relying solely on error messages.

Bad:

{
  "message": "Something went wrong"
}
Enter fullscreen mode Exit fullscreen mode

Better:

{
  "error": {
    "code": "EMAIL_ALREADY_EXISTS",
    "message": "Email already exists"
  }
}
Enter fullscreen mode Exit fullscreen mode

Frontend applications can now implement logic using the code instead of parsing text.

if (error.code === "EMAIL_ALREADY_EXISTS") {
  showEmailError();
}
Enter fullscreen mode Exit fullscreen mode

Messages may change.

Codes should not.


Return Metadata Separately

Avoid mixing business data with metadata.

Bad:

{
  "users": [...],
  "page": 1,
  "total": 500,
  "limit": 20
}
Enter fullscreen mode Exit fullscreen mode

Better:

{
  "data": [...],
  "meta": {
    "page": 1,
    "limit": 20,
    "total": 500
  }
}
Enter fullscreen mode Exit fullscreen mode

This becomes especially useful when implementing pagination, filtering, sorting, and analytics.


Timestamps Should Be ISO 8601

Bad:

{
  "created_at": "06/21/2026 14:30"
}
Enter fullscreen mode Exit fullscreen mode

Good:

{
  "created_at": "2026-06-21T14:30:00Z"
}
Enter fullscreen mode Exit fullscreen mode

ISO 8601 avoids timezone confusion and is supported by nearly every platform.


Avoid Null When Possible

Many APIs return:

{
  "items": null
}
Enter fullscreen mode Exit fullscreen mode

Now clients must handle:

  • null
  • undefined
  • empty array

Instead:

{
  "items": []
}
Enter fullscreen mode Exit fullscreen mode

An empty collection is often easier to reason about than a nullable collection.


Pagination Should Be Explicit

A paginated response should provide enough information for clients to continue fetching.

{
  "data": [...],
  "meta": {
    "page": 2,
    "limit": 20,
    "total": 100,
    "has_next": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Or cursor-based pagination:

{
  "data": [...],
  "next_cursor": "eyJpZCI6MTIzfQ=="
}
Enter fullscreen mode Exit fullscreen mode

This avoids guesswork on the client side.


Don't Leak Internal Errors

Never expose raw stack traces.

Bad:

{
  "error": "SQLSTATE[23000]: Integrity constraint violation..."
}
Enter fullscreen mode Exit fullscreen mode

Better:

{
  "error": {
    "code": "INTERNAL_SERVER_ERROR",
    "message": "Something went wrong"
  }
}
Enter fullscreen mode Exit fullscreen mode

Log details internally.

Return safe information externally.


Versioning Matters

APIs live longer than most developers expect.

Instead of breaking existing clients:

/api/v1/users
/api/v2/users
Enter fullscreen mode Exit fullscreen mode

or

Accept: application/vnd.company.v2+json
Enter fullscreen mode Exit fullscreen mode

A versioning strategy saves painful migrations later.


Example Final Response Format

Success:

{
  "success": true,
  "data": {
    "id": 1,
    "name": "John Doe"
  },
  "meta": {
    "request_id": "req_123456"
  }
}
Enter fullscreen mode Exit fullscreen mode

Error:

{
  "success": false,
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "User not found"
  },
  "meta": {
    "request_id": "req_123456"
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice the request_id.

When a customer reports an issue, support can immediately locate the exact request in logs.


Final Thoughts

A good API response format is not about aesthetics.

It is about reducing complexity across every consumer of the API:

  • Frontend applications
  • Mobile applications
  • QA automation
  • Integrations
  • AI agents
  • Future developers

The best API responses are boring, predictable, and easy to reason about.

Your future teammates will thank you for it.


Tip: When designing or debugging APIs, inspect the actual HTTP traffic instead of relying only on application logs. Seeing the raw request and response often reveals inconsistencies, missing fields, incorrect status codes, and serialization issues much faster.

HTTP Traffic Debugger for AI powered teams | NetworkSpy

Inspect, debug, and understand modern API traffic with NetworkSpy, the network debugger built for GraphQL, streaming, and AI applications.

favicon networkspy.app

Top comments (0)