How to Return Clean JSON Responses in PHP (With Working Examples)
A reliable PHP API starts with predictable HTTP status codes and JSON bodies. Here is a small helper you can run locally.

Returning JSON is easy. Returning JSON consistently is the part that saves time six months later. If one endpoint sends {"ok": 1}, another prints plain text, and a third returns HTTP 200 when something fails, frontend code becomes a collection of special cases. A small response contract gives callers one predictable place to look.
This walkthrough uses plain PHP 8.1+ and no framework. You can adapt the pattern to an existing PHP project, but avoid changing a live API response shape without checking the clients that already depend on it.
1. Choose one JSON response shape
A compact format is enough for many projects: success tells the caller whether the operation worked; message is a short, human-readable explanation; and data holds a result or null. Keep these keys and types stable across endpoints.
{
"success": true,
"message": "Profile loaded",
"data": {"id": 42, "displayName": "Alex"}
}That is an example contract, not a standard required by every API. If your existing frontend expects a different format, follow its established contract instead.
2. Create a reusable response function
Save the following as response.php. PHP encodes the JSON body before sending headers, so an encoding error does not accidentally produce a half-written response. The never return type is available in PHP 8.1 and newer.
<?php
declare(strict_types=1);
function jsonResponse(
int $status,
bool $success,
string $message,
?array $data = null
): never {
$body = json_encode(
['success' => $success, 'message' => $message, 'data' => $data],
JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo $body;
exit;
}3. Match the HTTP status to the result
| Status | Typical situation | What the frontend should do |
|---|---|---|
| 200 | Successful request | Read JSON data |
| 201 | Resource created | Use new resource identifier |
| 400 | Malformed request | Show request error |
| 401 | Missing or invalid authentication | Ask user to sign in |
| 403 | Authenticated but not allowed | Display access error |
| 404 | Resource not found | Show not-found UI |
| 422 | Well-formed but invalid fields | Show field-specific feedback |
| 500 | Unexpected server failure | Show generic error; log details |
An API should not return status 200 with success: false for every failure. HTTP clients, retry policies, monitoring tools and caches often rely on the real status code.
4. Use the helper inside an endpoint
Here is a small read-only endpoint. It explicitly allows only GET requests and uses the same helper for success and error cases. The sample output is fixed, so you can test response handling without adding a database first.
<?php
declare(strict_types=1);
require __DIR__ . '/response.php';
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header('Allow: GET');
jsonResponse(405, false, 'Method not allowed');
}
$id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT);
if ($id === false || $id === null || $id < 1) {
jsonResponse(422, false, 'A valid positive id is required');
}
// Replace this sample with an authorized database lookup.
if ($id !== 42) {
jsonResponse(404, false, 'Profile not found');
}
jsonResponse(200, true, 'Profile loaded', [
'id' => 42,
'displayName' => 'Alex'
]);
5. Check both status and JSON with curl
Run a local PHP server in the folder containing both files. Then request the example using curl. -i displays the response headers, including the HTTP status and content type.
php -S 127.0.0.1:8000
# In another terminal:
curl -i "http://127.0.0.1:8000/profile.php?id=42"
curl -i "http://127.0.0.1:8000/profile.php?id=abc"
curl -i "http://127.0.0.1:8000/profile.php?id=99"The first request should be 200, the second 422, and the third 404. In each case, inspect the Content-Type: application/json header and the keys in the body. The built-in PHP server is meant for local development, not for running a public production service.
6. Common mistakes worth avoiding
- Echoing debug text or warnings before JSON, which makes the response invalid and may expose private server details.
- Using a success status for failed authentication, because a custom
successflag cannot replace HTTP semantics. - Returning raw exception messages or SQL queries to public clients. Log internal details server-side instead.
- Changing
datafrom an array to a string on some routes without documenting it. - Assuming this helper performs authentication or validation. It formats responses; it does not decide who is allowed to access information.
Frequently asked questions
Should I use an array or object in data?
Use the type that matches the actual resource. In PHP, a list normally becomes a JSON array and an associative array becomes a JSON object. Test empty lists and empty objects, because their JSON representations are different.
Can I use this helper with an existing login API?
Yes, but preserve existing field names and status conventions where clients already depend on them. Introduce a new version or migration plan when you need to change the response contract.
Should an API always include a message?
Not necessarily. Some APIs use standardized error objects or only return resources. The important part is a documented, consistent format that solves a real consumer need.