Apuntes DAM
Tema 6 de 7DWES · 2º DAW

Desarrollo Web en Entorno Servidor

Servicios Web y APIs REST

REST, SOAP y GraphQL, diseño de una API REST, implementación en PHP con JSON y códigos de estado, autenticación con tokens, CORS, OpenAPI, consumo de APIs externas, aplicaciones híbridas y la misma API en Node.js.

70 min lecturaAvanzado

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

La carta
Dice qué puedes pedir y cómo: GET /productos, POST /pedidos. No explica cómo se cocina.
El pedido
Una petición HTTP con los datos en JSON. Da igual si el cliente es una app, una web o un script.
El plato
Una respuesta con un código de estado y los datos en JSON, siempre con el mismo formato.
RESTSOAPGraphQL
FormatoNormalmente JSONXML con un sobre (envelope)JSON
ContratoConvenciones de HTTP; opcionalmente OpenAPIWSDL, estrictoEsquema de tipos
Cómo se pideUna URL por recurso y los métodos HTTPSiempre POST a un único puntoUna consulta que dice qué campos quiere
Dónde lo verásLa inmensa mayoría de APIs públicas y privadasBanca, administración pública, sistemas antiguosAplicaciones con muchas pantallas y datos relacionados
¿Qué es una API? - La mejor explicación en español — EDteam

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ónAcciónRespuesta correcta
GET /api/productos?categoria=teclados&page=2Listar (con filtros y paginación)200 y el array
GET /api/productos/7Obtener uno200 o 404
POST /api/productosCrear201, el recurso creado y la cabecera Location
PUT /api/productos/7 · PATCH /api/productos/7Reemplazar · modificar parte200 con el recurso o 204
DELETE /api/productos/7Borrar204 sin cuerpo
GET /api/clientes/3/pedidosRecursos anidados200

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).

REST y RESTful APIs | Te lo explico en 5 minutos! — Leonardo Kuffo

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.

public/api.php
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

Una API sin documentación no la usa nadie. El estándar es OpenAPI (antes Swagger): un fichero YAML que describe cada ruta, sus parámetros y sus respuestas, y del que se genera una página interactiva para probarla. Para probar mientras desarrollas, usa Postman, Bruno o la extensión REST Client de VS Code.

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.

php
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&current=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);
  1. Pon siempre un tiempo máximo: si el otro servicio se cuelga, tu página no debe colgarse con él.
  2. Guarda en caché las respuestas que cambian poco (el tiempo, un tipo de cambio) para no repetir la llamada en cada visita.
  3. Trata los datos externos como no confiables: valida lo que recibes y escápalo al mostrarlo.
  4. 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.

server.js
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.

🐘PHPUna API REST de librosDifícil

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"}.

⏳
Consultar
⏳
Crear y borrar
⏳
Errores de método y ruta
⏳
Test oculto #4
0/4 tests pasados
🐘PHPResumir la respuesta de una APIMedio

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».

⏳
Repositorios
⏳
JSON roto
⏳
Test oculto #3
0/3 tests pasados

¿Has terminado este tema?

Crea una cuenta gratis para guardar qué temas has terminado, subir de nivel y ganar medallas.

Guardar mi progreso