Kodokon kodokon.com

Construir una API JSON en PHP

Construye una API JSON rigurosa dominando las cabeceras, la lectura del cuerpo en bruto y la semántica precisa de los códigos de estado.

9 min · 3 preguntas

Abrir esta lección en Kodokon

Una API JSON es un contrato: cada respuesta anuncia su tipo (Content-Type: application/json), lleva el código de estado correcto y una estructura estable. PHP no envía nada de esto por defecto - declara text/html. Primer bloque de construcción: un único helper de respuesta, tipado como never (PHP 8.1+). Este tipo de retorno garantiza, tanto al motor como a las herramientas de análisis estático, que la función nunca devuelve el control: cualquier código escrito después de una llamada es demostrablemente inalcanzable.

PHP
<?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']);
Un único punto de salida: estado, cabecera y cuerpo siempre coherentes.

Una segunda sutileza, descubierta a menudo demasiado tarde: $_POST permanece vacío ante un cuerpo JSON. PHP solo lo rellena para application/x-www-form-urlencoded y multipart/form-data. El cuerpo en bruto se lee desde el flujo php://input. Decodifícalo con JSON_THROW_ON_ERROR: sin ese flag, json_decode devuelve null silenciosamente en caso de error - indistinguible de un cuerpo que contiene literalmente null. Y gracias al tipo never, el switch de abajo no necesita ningún break.

PHP
<?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
        );
}
Supone que jsonResponse está definida arriba; el catch sin variable es de PHP 8.

Elige los códigos con precisión, porque tus clientes programarán contra ellos, no contra tus mensajes: 200 lectura exitosa, 201 creación (acompañada de una cabecera Location que apunta al recurso), 204 éxito sin cuerpo, 400 JSON mal formado, 404 recurso inexistente, 405 método no gestionado (con una cabecera Allow que enumera los permitidos), y 422 para un JSON sintácticamente válido pero semánticamente incorrecto. Compruébalo todo con curl -i, que muestra las cabeceras recibidas.

BASH
curl -i -X POST http://localhost:8000/items \
  -H 'Content-Type: application/json' \
  -d '{"name":"Widget"}'
La opción -i muestra el estado y las cabeceras: la única prueba real del contrato.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. Tu API recibe un POST con un cuerpo JSON: ¿por qué está vacío $_POST?
    • PHP solo rellena $_POST para cuerpos form-urlencoded y multipart; un cuerpo JSON se lee desde php://input
    • El JSON supera el límite post_max_size por defecto
    • Primero tienes que llamar a json_decode sobre $_POST para rellenarlo
  2. ¿Qué código de estado deberías devolver tras una creación exitosa mediante POST?
    • 200
    • 201
    • 204
    • 302
  3. ¿Qué garantiza el tipo de retorno never?
    • La función siempre devuelve null
    • La función nunca devuelve el control: termina con exit o una excepción, y el motor lo verifica
    • La función no puede ser sobrescrita por una clase hija