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
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
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
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
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, andbody. - Success responses must use a
datakey. - Validation errors must use an
errorskey. - 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
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.