# Standardizing API responses: a consistent contract

> How returning the same response structure from every endpoint simplifies client code and makes errors predictable, with a PHP wrapper class.

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

---
> **TL;DR:** When several clients use the same API, an endpoint that returns its own response shape leaves hidden debt in client code. The fix is a single contract with success, data, message and errors fields, kept consistent with HTTP status codes. On an API with live clients, the contract is applied only to new endpoints.

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](/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`](/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.
