# Standardizing API responses: a consistent contract

> How returning the same response structure from every endpoint simplifies client code and makes errors predictable.

- Published: 2020-01-05
- Category: Web Development
- Tags: API
- Reading time: 3 min read
- Source: https://www.muhammetsafak.com.tr/en/blog/standardizing-api-responses-a-consistent-contract/
- Language: en-US
- Author: Muhammet Şafak

---
When multiple clients are running against the same API — an iOS app, a React SPA, another service — what happens when each endpoint returns a different response shape? Every client writes its own parsing logic, a separate `if` block gets added for each error format, one endpoint returns a `data` key while another returns `result`. Over time, this inconsistency becomes hidden debt that accumulates on top of client code.

I ran into this problem firsthand across several projects over the past year. The solution doesn't require changing your technology stack: it's enough to write the contract into the API itself, not around it.

## The anatomy of a consistent response structure

A good API response contract guarantees three things: whether the outcome was a success or failure, the payload being carried, and error details if any exist. A simple skeleton that covers all three:

```json
{
  "success": true,
  "data": {},
  "message": null,
  "errors": null
}
```

A failed response carries `success: false`, `data: null`, a short description in `message`, and field-level errors in `errors`. The client only ever needs to check the `success` key — everything else is consistent.

## A response wrapper class in PHP

Rather than writing this structure by hand in every controller, I moved it into a single class:

```php
<?php

class ApiResponse
{
    public static function success($data = null, string $message = null, int $status = 200): \Illuminate\Http\JsonResponse
    {
        return response()->json([
            'success' => true,
            'data'    => $data,
            'message' => $message,
            'errors'  => null,
        ], $status);
    }

    public static function error(string $message, array $errors = [], int $status = 422): \Illuminate\Http\JsonResponse
    {
        return response()->json([
            'success' => false,
            'data'    => null,
            'message' => $message,
            'errors'  => $errors ?: null,
        ], $status);
    }
}
```

Usage in a controller is straightforward:

```php
public function store(Request $request)
{
    $validated = $request->validate([
        'name'  => 'required|string|max:255',
        'email' => 'required|email|unique:users',
    ]);

    $user = User::create($validated);

    return ApiResponse::success($user, 'User created.', 201);
}
```

When a [validation error](/en/blog/laravel-form-validation-in-practice/) occurs, you need to catch Laravel's own `ValidationException` and return it through the same contract. Customizing `Handler.php` is enough for this — one place covers all API errors.

## The relationship between HTTP status codes and the `success` flag

Some teams argue "we already have 2xx/4xx, we don't need a `success` flag." In my experience, that's not quite right: codes like 207 (Multi-Status), 422, and 409 are interpreted differently depending on the client library; the `success` flag eliminates that ambiguity. On the other hand, ignoring HTTP status codes altogether is also a mistake: returning errors with a 200 causes caching and logging issues on the client side. The two should be consistent with each other.

## Pagination and list responses

For endpoints that return lists, it's better practice to wrap the data with pagination metadata rather than returning a plain array:

```json
{
  "success": true,
  "data": {
    "items": [],
    "meta": {
      "current_page": 1,
      "per_page": 15,
      "total": 243,
      "last_page": 17
    }
  },
  "message": null,
  "errors": null
}
```

Laravel's [`LengthAwarePaginator`](/en/blog/pagination-filtering-and-sorting-in-rest-apis/) already produces this metadata; the only work is moving it under `data.meta`.

## Bringing the contract to the team: the backward compatibility question

The biggest benefit of defining the contract at the start of a project is that it prevents arguments down the road. But retrofitting a contract onto an existing API is a different challenge: there are live clients, and they expect a specific structure. At that point, two options remain: bump the version (`/v2/`) or apply the standard contract only to new endpoints.

I chose the second path. I left existing endpoints untouched and applied the contract to everything I added going forward. I wrote an adapter layer on the client side to abstract both. It's a temporary burden, but far less risky than breaking existing clients.

## Documenting the contract

No matter how solid the contract is, a second developer won't know about it if it isn't documented. Writing an OpenAPI (Swagger) schema makes a huge difference here; the response structure is defined once and every endpoint references it. I don't have a full OpenAPI integration yet, but at minimum I keep three example responses in the `README.md`.

---

A consistent response contract isn't a magic solution, but the return on investment far exceeds the cost. Client code becomes simpler, error scenarios become predictable, and when a new endpoint gets added there's no debate about structure. Making this decision at the start of a project is much easier than going back to fix it later.
