1. Cuando el cliente no es un navegador
Hasta ahora el servidor devolvía páginas HTML pensadas para personas. Pero muchas veces quien pide los datos es otro programa: la app móvil de la tienda, una SPA hecha en React, el sistema de un proveedor o un servicio de pagos. Para ellos, el HTML sobra: necesitan datos estructurados. Un servicio web es precisamente eso, una aplicación que expone sus funciones a otros programas a través de HTTP.
Una API es la carta de un restaurante
| REST | SOAP | GraphQL | |
|---|---|---|---|
| Formato | Normalmente JSON | XML con un sobre (envelope) | JSON |
| Contrato | Convenciones de HTTP; opcionalmente OpenAPI | WSDL, estricto | Esquema de tipos |
| Cómo se pide | Una URL por recurso y los métodos HTTP | Siempre POST a un único punto | Una consulta que dice qué campos quiere |
| Dónde lo verás | La inmensa mayoría de APIs públicas y privadas | Banca, administración pública, sistemas antiguos | Aplicaciones con muchas pantallas y datos relacionados |
2. Diseñar una API REST
REST no es una librería ni un estándar cerrado, sino un estilo de diseño que aprovecha HTTP en lugar de reinventarlo. Sus ideas clave: todo son recursos identificados por URL (en plural y con sustantivos, no verbos), las acciones se expresan con los métodos HTTP, el resultado se comunica con los códigos de estado y cada petición lleva todo lo necesario para entenderla (el servidor no guarda estado entre peticiones).
| Petición | Acción | Respuesta correcta |
|---|---|---|
| GET /api/productos?categoria=teclados&page=2 | Listar (con filtros y paginación) | 200 y el array |
| GET /api/productos/7 | Obtener uno | 200 o 404 |
| POST /api/productos | Crear | 201, el recurso creado y la cabecera Location |
| PUT /api/productos/7 · PATCH /api/productos/7 | Reemplazar · modificar parte | 200 con el recurso o 204 |
| DELETE /api/productos/7 | Borrar | 204 sin cuerpo |
| GET /api/clientes/3/pedidos | Recursos anidados | 200 |
Un error también es una respuesta: devuelve el código adecuado y un cuerpo JSON coherente, siempre con la misma forma, para que el cliente pueda mostrar el mensaje. 400 si la petición está mal formada, 401 si falta autenticación, 403 si no tiene permiso, 404 si no existe, 422 si los datos no pasan la validación y 500 si falla el servidor (sin mostrar detalles internos).
3. Una API REST en PHP
Para devolver JSON en lugar de HTML basta con indicar el tipo de contenido, fijar el código de estado y escribir el resultado de json_encode. Los datos que envía el cliente en el cuerpo (en JSON) no llegan a $_POST: se leen en crudo de php://input y se decodifican.
1<?php
2declare(strict_types=1);
3
4header('Content-Type: application/json; charset=utf-8');
5
6function responder(int $codigo, mixed $datos = null): never
7{
8 http_response_code($codigo);
9 if ($datos !== null) {
10 echo json_encode($datos, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
11 }
12 exit;
13}
14
15$metodo = $_SERVER['REQUEST_METHOD'];
16$ruta = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
17$repo = new ProductoRepositorio(conectar());
18
19if ($ruta === '/api/productos' && $metodo === 'GET') {
20 responder(200, $repo->todos());
21}
22
23if ($ruta === '/api/productos' && $metodo === 'POST') {
24 $datos = json_decode(file_get_contents('php://input'), true) ?? [];
25 $errores = validarProducto($datos);
26 if ($errores) {
27 responder(422, ['errores' => $errores]);
28 }
29 $producto = $repo->crear($datos);
30 header('Location: /api/productos/' . $producto['id']);
31 responder(201, $producto);
32}
33
34if (preg_match('#^/api/productos/(\d+)$#', $ruta, $m)) {
35 $producto = $repo->buscar((int) $m[1]);
36 if ($producto === null) {
37 responder(404, ['error' => 'Producto no encontrado']);
38 }
39 if ($metodo === 'GET') {
40 responder(200, $producto);
41 }
42 if ($metodo === 'DELETE') {
43 $repo->borrar($producto['id']);
44 responder(204);
45 }
46 responder(405, ['error' => 'Método no permitido']);
47}
48
49responder(404, ['error' => 'Ruta no encontrada']);Proteger la API
Una API no suele usar la sesión con cookies de un navegador, sino un token que el cliente envía en cada petición en la cabecera Authorization: Bearer …. Puede ser una clave aleatoria guardada en la base de datos o un JWT, un token firmado que contiene los datos del usuario y su caducidad y que el servidor comprueba sin consultar la base de datos. Laravel Sanctum y los paquetes de JWT resuelven esta parte.
Si la API la consume una web de otro dominio, el navegador aplicará la política del mismo origen. Para permitirlo, la API responde con cabeceras CORS como Access-Control-Allow-Origin: https://mitienda.com, y atiende las peticiones previas OPTIONS que el navegador envía antes de un POST con JSON.
Documenta la API
4. Consumir APIs de otros y aplicaciones híbridas
El servidor también puede ser cliente de otros servicios: cobrar con Stripe o Redsys, calcular un envío con la API de una empresa de transporte, mostrar el tiempo, validar un NIF o traducir un texto. Combinar datos propios con los de servicios externos da lugar a las llamadas aplicaciones híbridas o mashups.
1// Con funciones nativas
2$contexto = stream_context_create(['http' => ['timeout' => 5, 'header' => "Accept: application/json\r\n"]]);
3$respuesta = @file_get_contents('https://api.open-meteo.com/v1/forecast?latitude=42.46&longitude=-2.45¤t=temperature_2m', false, $contexto);
4if ($respuesta === false) {
5 // plan B: mostrar la página sin el tiempo, nunca un error en blanco
6}
7$tiempo = json_decode($respuesta, true);
8
9// Con Guzzle (composer require guzzlehttp/guzzle), más cómodo para APIs complejas
10$cliente = new GuzzleHttp\Client(['base_uri' => 'https://api.ejemplo.com/', 'timeout' => 5]);
11$datos = json_decode($cliente->get('pedidos', ['query' => ['estado' => 'pendiente']])->getBody(), true);- Pon siempre un tiempo máximo: si el otro servicio se cuelga, tu página no debe colgarse con él.
- Guarda en caché las respuestas que cambian poco (el tiempo, un tipo de cambio) para no repetir la llamada en cada visita.
- Trata los datos externos como no confiables: valida lo que recibes y escápalo al mostrarlo.
- Las claves de API van en variables de entorno, nunca en el código ni, por supuesto, en el JavaScript del cliente.
5. La misma API en Node.js
Para comprobar que las ideas son universales, esta es la ruta de listar y crear productos con Node.js y Express. Cambia la sintaxis, pero los conceptos son idénticos: rutas, métodos, códigos de estado y JSON.
1import express from "express";
2
3const app = express();
4app.use(express.json());
5
6app.get("/api/productos", async (req, res) => {
7 res.json(await productos.todos());
8});
9
10app.post("/api/productos", async (req, res) => {
11 const { nombre, precio } = req.body;
12 if (!nombre || precio < 0) return res.status(422).json({ error: "Datos no válidos" });
13 const nuevo = await productos.crear({ nombre, precio });
14 res.status(201).location("/api/productos/" + nuevo.id).json(nuevo);
15});
16
17app.listen(3000);6. Retos
En el primer reto construyes el enrutado de una API REST completa, con sus códigos de estado; en el segundo procesas la respuesta de una API externa, incluido el caso de que llegue rota.
Retos de PHP en el IDE
Escribe el código que falta y pulsa Ejecutar tests: se ejecuta con PHP 8.3 real y se prueba con varios casos, algunos ocultos.
Cada línea de la entrada es una petición «MÉTODO RUTA [cuerpo JSON]». Escribe para cada una el código de estado y, si hay cuerpo, un espacio y la respuesta en JSON (json_encode con JSON_UNESCAPED_UNICODE). GET /api/libros → 200 con la lista; GET /api/libros/{id} → 200 con el libro o 404 {"error":"Libro no encontrado"}; POST /api/libros con titulo y autor → 201 con el libro creado (id = mayor id + 1) o 422 {"error":"Faltan campos"}; DELETE /api/libros/{id} → 204 sin cuerpo o 404. Cualquier otro método sobre esas rutas → 405 {"error":"Método no permitido"} y cualquier otra ruta → 404 {"error":"Ruta no encontrada"}.
La entrada es la respuesta JSON de una API de repositorios: un array de objetos con name, language (puede ser null) y stars. Muestra los tres con más estrellas («1. nombre (★ 1.234)», con punto de miles), después «Total de estrellas: N» y «Lenguajes: …» con los lenguajes distintos ordenados alfabéticamente y separados por comas, sin contar null. Si el JSON no es válido, muestra «JSON no válido».