Skip to content

PHP Tutorial Working with JSON and APIs

Building a JSON Response

Lesson 308 of 369 2 min read By Deepak Paswan Updated Oct 5, 2026

A PHP script that serves an API sends JSON with the right headers and status code.

index.php
PHP
<?php
header("Content-Type: application/json; charset=utf-8");

echo json_encode(["status" => "ok", "time" => time()]);

Without the Content-Type header, the browser (or the app calling you) may treat the response as plain HTML.

A reusable helper

index.php
PHP
<?php
function jsonResponse(mixed $data, int $status = 200): string
{
    // in a real script: http_response_code($status); header("Content-Type: application/json; charset=utf-8");
    return json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}

echo jsonResponse(["id" => 7, "name" => "Ana"]);
Output
OUTPUT
{"id":7,"name":"Ana"}

The real version also sends headers and stops the script.

api-helpers.php
PHP
<?php

declare(strict_types=1);

function sendJson(mixed $data, int $status = 200): never
{
    http_response_code($status);
    header("Content-Type: application/json; charset=utf-8");
    header("X-Content-Type-Options: nosniff");

    echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
    exit;
}

The return type never says that this function does not come back. Use it like this:

index.php
PHP
<?php
require __DIR__ . "/api-helpers.php";

$product = findProduct((int) ($_GET["id"] ?? 0));

if ($product === null) {
    sendJson(["error" => "Product not found"], 404);
}

sendJson($product);

Consistent error format

Clients can handle errors easily if every error looks the same.

index.php
PHP
<?php
function errorBody(string $code, string $message, array $details = []): array
{
    $body = ["error" => ["code" => $code, "message" => $message]];

    if ($details !== []) {
        $body["error"]["details"] = $details;
    }

    return $body;
}

echo json_encode(errorBody("validation_failed", "The data is not valid.", ["email" => "Invalid email"]));
Output
OUTPUT
{"error":{"code":"validation_failed","message":"The data is not valid.","details":{"email":"Invalid email"}}}

Pair it with the right status code (see HTTP Status Codes): 400 or 422 for validation, 404 for missing, 500 for server errors. Never include stack traces or SQL in the message.

Lists with pagination info

index.php
PHP
<?php
$items = [["id" => 1], ["id" => 2]];

echo json_encode([
    "data" => $items,
    "meta" => ["page" => 1, "per_page" => 2, "total" => 11],
]);
Output
OUTPUT
{"data":[{"id":1},{"id":2}],"meta":{"page":1,"per_page":2,"total":11}}

Return an object at the top level ({"data": [...]}), not a bare array. It lets you add fields later without breaking clients.

Do not print anything else

Any stray output (a warning, an echo, a space before <?php) corrupts the JSON. Keep display_errors off in an API, and log errors instead.

CORS

When JavaScript on another website calls your API from a browser, the browser checks CORS headers. A simple setup for one trusted site:

index.php
PHP
<?php
$allowed = ["https://app.example.com"];
$origin = $_SERVER["HTTP_ORIGIN"] ?? "";

if (in_array($origin, $allowed, true)) {
    header("Access-Control-Allow-Origin: " . $origin);
    header("Vary: Origin");
    header("Access-Control-Allow-Headers: Content-Type, Authorization");
    header("Access-Control-Allow-Methods: GET, POST, PUT, DELETE");
}

if ($_SERVER["REQUEST_METHOD"] === "OPTIONS") {   // the browser's "preflight" check
    http_response_code(204);
    exit;
}

Never use Access-Control-Allow-Origin: * together with cookies or private data. Allow only origins you trust.

Caching

Add Cache-Control for responses that can be cached (public, max-age=60), and no-store for private data.

lightbulb
Test with a client
Use the browser’s Network tab, curl -i https://yoursite.example/api.php, or a tool such as Postman. Look at the status code and the headers, not only the body.