← Servidor web y API con Node

Sesión 1

Diseñar la API

Tres horas. Media hora para entender qué distingue una API diseñada de un conjunto de rutas, y dos horas y media escribiendo el contrato de la tuya y reorganizando el proyecto para poder cumplirlo.

Hoy · Hoja de ruta

  1. 1. Aprende: Qué significa orientar una API a recursos, qué piezas forman su contrato y cómo se reparte una aplicación Express en ficheros.
  2. 2. Haz: Analiza dos APIs reales, escribe el contrato completo de la tuya y reorganiza el proyecto con routers.
  3. 3. Comprueba: Otra persona podría escribir el cliente solo con tu contrato, y tu aplicación se puede crear sin arrancarla.

Antes de empezar · 5 minutos, sin apuntes

  1. Escribe las rutas que tiene hoy tu servidor. Si mañana alguien más las usara, ¿podría adivinar la siguiente?
  2. ¿Cómo sabría un cliente distinguir «no existe» de «se rompió»?
  3. ¿Cómo probarías tu aplicación sin levantar un servidor?

Se explica

25 minutos · conceptos y demostración

Tu servidor ya funciona. Hoy se convierte en algo que otra persona puede usar sin preguntarte: un contrato escrito, cumplido de forma constante, sobre una estructura que admite crecer.

Recursos, no acciones

Recurso

Una cosa del problema que se puede identificar con una URL: un producto, una categoría, un pedido. La ruta nombra el recurso; el método dice qué se hace con él.

Con acciones en la ruta Orientado a recursos
GET /obtenerProductos GET /api/productos
GET /verProducto?id=7 GET /api/productos/7
POST /crearProducto POST /api/productos
POST /borrarProducto DELETE /api/productos/7

La columna derecha tiene una propiedad que la izquierda no: es predecible. Quien conozca dos rutas sabe escribir la tercera.

Lo que hace REST a una API
  1. Las cosas se identifican con URLs
  2. El método dice la operación
  3. El código de estado dice el resultado
  4. Cada petición se entiende sola, sin estado guardado entre ellas
  5. La representación es JSON, con una forma constante

Sin estado

El servidor no recuerda nada entre una petición y la siguiente. Todo lo necesario viaja en la petición. Es lo que permite que dos copias del servidor atiendan al mismo cliente sin coordinarse, y es la razón de que la autenticación se resuelva con algo que se envía en cada llamada.

Regla de nombrado Ejemplo
Sustantivos en plural /api/productos, no /api/producto
Minúsculas y guiones /api/categorias-destacadas
Jerarquía para lo que pertenece /api/categorias/teclados/productos
Filtros en la consulta /api/productos?categoria=teclados&max=100
Sin verbos El verbo ya es el método

Filtrar no es una ruta nueva

Los productos baratos no son un recurso distinto de los productos: son los mismos, filtrados. Van en la cadena de consulta, no en la ruta.

La prueba: si al añadir un filtro nuevo tienes que crear una ruta nueva, acabarás con quince rutas que devuelven lo mismo con distinta condición.

El contrato

Pieza Qué fija
Ruta y método Cómo se pide cada operación
Cuerpo de la petición Qué campos se envían y de qué tipo
Código de estado Qué ha pasado
Cuerpo de la respuesta Qué campos vuelven y con qué nombre
Formato de error La misma forma, siempre
Método Ruta Éxito Errores
GET /api/productos 200 500
GET /api/productos/:id 200 400, 404
POST /api/productos 201 400
PUT /api/productos/:id 200 400, 404
PATCH /api/productos/:id 200 400, 404
DELETE /api/productos/:id 204 400, 404

El 400 de las rutas con identificador no es un capricho: /api/productos/abc es una petición mal formada, y responder 404 a eso confunde «no existe» con «no me has pedido bien».

{
  "error": "Los datos no son válidos",
  "codigo": "VALIDACION",
  "detalles": [
    { "campo": "precio", "mensaje": "Debe ser un número mayor que cero" },
    { "campo": "nombre", "mensaje": "Es obligatorio" }
  ]
}

Un solo formato de error, en toda la API

Si un error llega a veces como texto plano, a veces como { error } y a veces como { mensaje }, el cliente necesita un tratamiento distinto para cada ruta, y ninguno para las que todavía no existen.

Con una forma única, el cliente escribe una función de tratamiento de errores y vale para toda la API, hoy y cuando crezca. El campo codigo permite además reaccionar por programa sin depender del texto, que puede traducirse o reescribirse.

Método Repetirlo dos veces
GET No cambia nada
PUT Deja el mismo resultado
DELETE El segundo no borra nada nuevo
POST Crea otro

Esa propiedad —que repetir la operación no cambie el resultado— es la que permite que un cliente reintente sin miedo cuando la red falla. Con POST no se puede, y por eso los formularios que se envían dos veces crean dos pedidos.

Un router por recurso

// src/rutas/productos.js
import { Router } from "express";

const router = Router();

router.get("/", listarProductos);
router.get("/:id", obtenerProducto);
router.post("/", crearProducto);
router.put("/:id", reemplazarProducto);
router.delete("/:id", borrarProducto);

export default router;
// src/app.js
import express from "express";
import productos from "./rutas/productos.js";

export function crearApp() {
  const app = express();

  app.use(express.json({ limit: "100kb" }));
  app.use(registro);
  app.use(express.static("publico"));

  app.use("/api/productos", productos);

  app.use(noEncontrado);
  app.use(manejadorDeErrores);
  return app;
}

Las rutas del router son relativas al punto donde se monta, así que la ruta base aparece una sola vez. Cambiar /api/productos por /api/v1/productos es cambiar una línea.

// src/servidor.js
import { crearApp } from "./app.js";

const PUERTO = Number(process.env.PUERTO ?? 3000);
crearApp().listen(PUERTO, () => console.log(`Escuchando en el puerto ${PUERTO}`));

Quien crea la aplicación no debe arrancarla

Si app.js llama a listen, importarlo desde una prueba levanta un servidor de verdad, ocupa un puerto y obliga a apagarlo después.

Con la aplicación por un lado y el arranque por otro, las pruebas de la sesión 5 importan la aplicación y le hacen peticiones sin abrir ningún puerto. Es una decisión de dos líneas que decide si tu proyecto se puede probar.

El orden de la cadena
  1. Analizador del cuerpo y registro
  2. Estáticos
  3. Routers de la API
  4. 404 para lo que no encajó
  5. Manejador de errores, siempre el último

Se trabaja

150 minutos · el contrato y la estructura

Hoy se escribe antes de programar, que es el orden que esta unidad exige. Los dos últimos pasos comprueban las dos promesas de la sesión: que el contrato baste y que la aplicación se pueda probar.

Paso 1 · Analizar y diseñar · 35 min

  1. Explora dos APIs públicas y anota diez rutas de cada una.
  2. Clasifícalas: ¿orientadas a recursos o a acciones?
  3. Localiza cómo filtran, cómo paginan y cómo devuelven los errores.
  4. Diseña sobre papel la tabla de rutas de tu proyecto: método, ruta, qué hace, qué devuelve y con qué código.
  5. Añade un segundo recurso que tenga relación con el primero, y decide cómo se expresa esa relación en las rutas.

Paso 2 · El contrato escrito · 40 min

  1. Escribe la tabla completa de tu API, con éxitos y errores por ruta.
  2. Define el objeto que representa un producto: campos, tipos y cuáles son obligatorios.
  3. Define el formato único de error, con sus códigos.
  4. Añade paginación al contrato y decide qué devuelve exactamente la respuesta paginada.
  5. Escribe peticiones.http con un ejemplo de cada caso, incluidos los que fallan.

Antes de continuar: para cada ruta con identificador, el contrato distingue el 400 del 404 y dice cuándo se devuelve cada uno.

Paso 3 · Reorganizar · 40 min

  1. Crea src/app.js con crearApp() y deja servidor.js solo con el arranque.
  2. Mueve las rutas de productos a su router.
  3. Mueve el registro y los errores a src/middleware/.
  4. Comprueba que todo sigue funcionando con tu fichero de peticiones.
  5. Añade el router del segundo recurso, aunque esté vacío, y monta su ruta base.
  6. Monta la API bajo /api/v1 cambiando una sola línea. Si has tenido que tocar más de una, las rutas del router no eran relativas.

Paso 4 · Que otro escriba el cliente · 20 min

Dale a un compañero solo el contrato, sin acceso a tu código ni a tu servidor.

Encargo ¿Lo resolvió? Qué le faltó del contrato
Escribir la llamada que lista los productos de una categoría
Escribir la llamada que crea un producto
Decir qué recibe si el precio es inválido
Decir qué recibe si pide un identificador que no existe
Adivinar una ruta que no le has enseñado

Cada hueco de la tercera columna es un agujero de tu contrato, no un despiste de la persona. Corrígelo y vuelve a probarlo.

Paso 5 · La aplicación sin puerto · 15 min

  1. Escribe un pequeño programa que importe crearApp() y le haga una petición sin llamar a listen.
  2. Comprueba que ningún puerto queda ocupado: arráncalo dos veces seguidas y mira que no da EADDRINUSE.
  3. Ahora haz lo contrario: mete el listen dentro de app.js y repite el apartado 1. Anota qué ocurre.
  4. Devuelve el listen a su sitio.
  5. Escribe en un comentario por qué esta separación importa más de lo que parece. En la sesión 5 la vas a necesitar para cada prueba que escribas.

Ampliación si has completado el trabajo

Primero termina y comprueba los cinco pasos. Los dos retos tratan el contrato como lo que es: algo que otros usan y que no se puede cambiar a la ligera.

Reto 1 · La versión 2 sin romper la 1

Tu API ya la usa alguien. Ahora hay que cambiar algo que rompe el contrato: el campo precio pasa a ser un objeto con importe y moneda.

  1. Enumera todo lo que se rompería en un cliente que ya funciona.
  2. Decide una estrategia de versionado. Hay al menos tres: en la ruta, en una cabecera, o por parámetro. Busca qué hace cada una, elige y justifica.
  3. Implementa la que elijas de modo que /api/v1/productos siga devolviendo lo de antes y /api/v2/productos devuelva la forma nueva. Reutiliza la lógica: lo que cambia es la representación, no los datos.
  4. ¿Durante cuánto tiempo mantendrías la v1, y cómo avisarías de que va a desaparecer? Busca qué cabecera existe para eso.
  5. Hay cambios que no rompen el contrato y se pueden hacer sin versionar. Escribe tres ejemplos y explica por qué son seguros.
  6. Explica en tres líneas por qué esto no es una preocupación exagerada para un proyecto de clase. La respuesta tiene que ver con quién consume tu API a partir de la sesión 4: tu propio cliente.

Reto 2 · Una decisión discutible en una API real

Elige una API pública con documentación abierta y búscale una decisión de diseño que no seguirías.

  1. Documéntala: qué hace, con qué rutas, y qué te parece discutible.
  2. Busca si hay una razón histórica o técnica detrás. Muchas decisiones raras lo son por compatibilidad con algo anterior, y eso cambia el juicio.
  3. Escribe cómo la harías tú, con la tabla de rutas equivalente.
  4. Enumera qué perdería tu versión respecto a la original. Siempre se pierde algo: si no encuentras nada, no has entendido la decisión original.
  5. Busca en la misma API una decisión que al principio te pareciera rara y que, al mirarla despacio, resulte acertada. Explícala.
  6. Compara esa API con tu contrato: ¿qué le copiarías?
Objetivo mínimoLa tabla de rutas de tu proyecto con siete entradas, el contrato completo con su formato de error, y la aplicación separada del arranque.
Si lo tienesLa tabla del paso 4 contestada y sus agujeros corregidos, y la API montada bajo /api/v1 cambiando una línea.
RetoEl versionado implementado con las dos versiones conviviendo, o el análisis razonado de una API real.

Cierre

5 minutos · comprobación y recuerdo

Lista de verificación de la sesión

  • Tu API está diseñada por recursos y escrita como contrato.
  • Los filtros van en la cadena de consulta, no en rutas nuevas.
  • Todos los errores comparten forma, con su código propio.
  • Distingues 400 de 404 en la misma ruta.
  • Cada recurso tiene su router, con rutas relativas.
  • La aplicación se puede crear sin arrancarla.

Antes de cerrar · 3 minutos, sin mirar

  1. ¿Por qué las rutas no llevan verbos, y dónde van los filtros?
  2. ¿Qué significa que el servidor no guarde estado?
  3. ¿Qué código devuelve una creación correcta, y qué cabecera la acompaña?
  4. ¿Qué responde /api/productos/abc, y por qué no un 404?
  5. ¿Qué operaciones se pueden repetir sin consecuencias, y cuál no?
  6. ¿Por qué la aplicación se crea en un fichero y se arranca en otro?
Ver respuestas

1 · Porque el verbo ya lo aporta el método HTTP; los filtros van en la cadena de consulta, porque no son recursos distintos.

2 · Que no recuerda nada entre peticiones: cada una trae todo lo que necesita.

3 · Un 201, con la cabecera Location.

4 · Un 400: la petición está mal formada, no es que el recurso no exista.

5 · GET, PUT y DELETE se pueden repetir; POST crea otro recurso cada vez.

6 · Para poder importarla en las pruebas sin ocupar un puerto.

Microprueba semanal 1 · 5–10 minutos

Individual, sin IA y sin apuntes.

  1. Reescribe orientadas a recursos: /obtenerProductos y /borrarProducto?id=7.
  2. Di qué código de estado devuelve cada operación del CRUD cuando va bien.
  3. ¿Por qué la aplicación se crea en un fichero y se arranca en otro?