← All tutorials

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.

PHP API code and success and error JSON response illustration
Illustration for this tutorial. The examples and requests shown are educational.

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.

JSON · success response
{
  "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 · response.php
<?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

StatusTypical situationWhat the frontend should do
200Successful requestRead JSON data
201Resource createdUse new resource identifier
400Malformed requestShow request error
401Missing or invalid authenticationAsk user to sign in
403Authenticated but not allowedDisplay access error
404Resource not foundShow not-found UI
422Well-formed but invalid fieldsShow field-specific feedback
500Unexpected server failureShow 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 · profile.php
<?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'
]);
Illustration showing PHP code passing structured requests and returning JSON responses
Use one contract for success and error responses so each client knows what to expect.

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.

Terminal · local testing
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 success flag cannot replace HTTP semantics.
  • Returning raw exception messages or SQL queries to public clients. Log internal details server-side instead.
  • Changing data from 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.

Editorial note: examples are provided for learning. Test them in your own environment and review security, privacy and compatibility requirements before using them in production.