Build a rigorous JSON API by mastering headers, reading the raw body and the fine-grained semantics of status codes.
Open this lesson in KodokonA JSON API is a contract: every response announces its type (Content-Type: application/json), carries the right status code and a stable structure. PHP sends none of this by default - it declares text/html. First building block: a single response helper, typed never (PHP 8.1+). This return type guarantees, to the engine as well as to static analysis tools, that the function never hands control back: any code written after a call is provably unreachable.
<?php
declare(strict_types=1);
function jsonResponse(
mixed $data,
int $status = 200
): never {
http_response_code($status);
header('Content-Type: application/json');
echo json_encode(
$data,
JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE
);
exit;
}
jsonResponse(['status' => 'ok']);A second subtlety, often discovered too late: $_POST stays empty in the face of a JSON body. PHP only fills it for application/x-www-form-urlencoded and multipart/form-data. The raw body is read from the php://input stream. Decode it with JSON_THROW_ON_ERROR: without that flag, json_decode silently returns null on error - indistinguishable from a body that literally contains null. And thanks to the never type, the switch below needs no break at all.
<?php
declare(strict_types=1);
switch ($_SERVER['REQUEST_METHOD']) {
case 'GET':
jsonResponse(['items' => []]);
case 'POST':
$raw = (string) file_get_contents(
'php://input'
);
try {
$payload = json_decode(
$raw,
true,
512,
JSON_THROW_ON_ERROR
);
} catch (JsonException) {
jsonResponse(
['error' => 'Malformed JSON'],
400
);
}
header('Location: /items/42');
jsonResponse(['created' => $payload], 201);
default:
header('Allow: GET, POST');
jsonResponse(
['error' => 'Method not allowed'],
405
);
}Choose the codes precisely, because your clients will program against them, not against your messages: 200 successful read, 201 creation (accompanied by a Location header pointing to the resource), 204 success with no body, 400 malformed JSON, 404 missing resource, 405 unhandled method (with an Allow header listing the permitted ones), and 422 for JSON that is syntactically valid but semantically incorrect. Check it all with curl -i, which displays the received headers.
curl -i -X POST http://localhost:8000/items \
-H 'Content-Type: application/json' \
-d '{"name":"Widget"}'