http clients and apis

Building JSON Responses

A JSON response is more than json_encode(). It needs the right status code, Content-Type, body shape, and error handling.

Clients become easier to write when your API uses consistent response conventions across endpoints.

Success response shape

PHP example
<?php

declare(strict_types=1);

$response = [
    'data' => ['id' => 123, 'name' => 'Notebook'],
    'meta' => ['request_id' => 'req_abc123'],
];

echo json_encode($response, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"data":{"id":123,"name":"Notebook"},"meta":{"request_id":"req_abc123"}}

In a real web response, also send Content-Type: application/json and 200 OK.

Create a small response helper

A helper can keep status, headers, and encoded body together.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array<string, mixed> $body
 * @return array{status: int, headers: array<string, string>, body: string}
 */
function jsonResponse(array $body, int $status = 200): array
{
    return [
        'status' => $status,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode($body, JSON_THROW_ON_ERROR),
    ];
}

$response = jsonResponse(['data' => ['id' => 123]], 200);

echo $response['status'] . ' ' . $response['headers']['Content-Type'] . ' ' . $response['body'] . PHP_EOL;

// Prints:
// 200 application/json {"data":{"id":123}}

Frameworks already provide response helpers. The point is to understand what they produce.

Error response shape

Use a predictable error structure.

PHP example
<?php

declare(strict_types=1);

$error = [
    'errors' => [
        [
            'code' => 'validation_failed',
            'field' => 'email',
            'message' => 'Enter a valid email address.',
        ],
    ],
];

echo json_encode($error, JSON_THROW_ON_ERROR) . PHP_EOL;

// Prints:
// {"errors":[{"code":"validation_failed","field":"email","message":"Enter a valid email address."}]}

Do not return raw exception messages from production APIs. Convert internal failures into safe messages and log details internally.

Match body and status code

The status code and body should tell the same story.

  • 200 OK: returned an existing resource or result.
  • 201 Created: created a resource; include the created resource or location.
  • 204 No Content: success with no body.
  • 400 Bad Request: malformed request, such as invalid JSON.
  • 401 Unauthorized: missing or invalid authentication.
  • 403 Forbidden: authenticated but not allowed.
  • 404 Not Found: resource not found.
  • 422 Unprocessable Content: validation failed.

For 204 No Content, do not send a JSON body.

PHP example
<?php

declare(strict_types=1);

/**
 * @return array{status: int, headers: array<string, string>, body: string}
 */
function noContentResponse(): array
{
    return ['status' => 204, 'headers' => [], 'body' => ''];
}

$response = noContentResponse();

echo $response['status'] . ' body length ' . strlen($response['body']) . PHP_EOL;

// Prints:
// 204 body length 0

Avoid accidental output

If a PHP warning, debug var_dump(), or stray whitespace is sent before your JSON response, the client may receive invalid JSON.

Keep API endpoints free from debug output. Use logs, not printed debugging text.

Encoding failures

json_encode() can fail. Use JSON_THROW_ON_ERROR and handle the exception at the framework or API boundary. If your application cannot encode its own response, that is normally a server error, not a client validation error.

What to check in a project

Check that JSON responses set Content-Type: application/json.

Check response body shape is consistent across endpoints.

Check 204 responses have no body.

Check validation errors include useful field-level details.

Check server errors do not leak stack traces or secrets.

Check debug output cannot corrupt JSON responses.

What you should be able to do

After this lesson, you should be able to build consistent JSON success and error responses, pair status codes with response bodies correctly, handle no-content responses, and avoid accidental non-JSON output.

Practice

Task: Build Response Helpers

Write a small PHP script with helpers for JSON success, JSON validation error, and no-content responses.

Requirements

  • Use declare(strict_types=1);.
  • Return arrays containing status, headers, and body.
  • Success responses must use a data key.
  • Validation errors must use an errors key.
  • No-content responses must have an empty body.
  • Encode JSON with JSON_THROW_ON_ERROR.
  • Print all three response summaries.

Check Your Work

Run the script and confirm the 204 response does not contain JSON.

Show solution

This solution keeps status, headers, and body together so callers cannot forget one part of the response.

PHP example
<?php

declare(strict_types=1);

/**
 * @param array<string, mixed> $data
 * @return array{status: int, headers: array<string, string>, body: string}
 */
function successResponse(array $data, int $status = 200): array
{
    return [
        'status' => $status,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode(['data' => $data], JSON_THROW_ON_ERROR),
    ];
}

/**
 * @return array{status: int, headers: array<string, string>, body: string}
 */
function validationErrorResponse(string $field, string $message): array
{
    return [
        'status' => 422,
        'headers' => ['Content-Type' => 'application/json'],
        'body' => json_encode([
            'errors' => [
                ['field' => $field, 'message' => $message],
            ],
        ], JSON_THROW_ON_ERROR),
    ];
}

/**
 * @return array{status: int, headers: array<string, string>, body: string}
 */
function noContentResponse(): array
{
    return ['status' => 204, 'headers' => [], 'body' => ''];
}

$responses = [
    successResponse(['id' => 123, 'name' => 'Notebook']),
    validationErrorResponse('name', 'Name is required.'),
    noContentResponse(),
];

foreach ($responses as $response) {
    echo $response['status'] . ' body length ' . strlen($response['body']) . PHP_EOL;
}

// Prints:
// 200 body length 37
// 422 body length 63
// 204 body length 0

Why This Works

The helpers enforce consistent response shapes. The 204 case proves successful responses do not always need JSON bodies.