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. 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. Haz: Analiza dos APIs reales, escribe el contrato completo de la tuya y reorganiza el proyecto con routers.
- 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
- Escribe las rutas que tiene hoy tu servidor. Si mañana alguien más las usara, ¿podría adivinar la siguiente?
- ¿Cómo sabría un cliente distinguir «no existe» de «se rompió»?
- ¿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.
- Las cosas se identifican con URLs
- El método dice la operación
- El código de estado dice el resultado
- Cada petición se entiende sola, sin estado guardado entre ellas
- 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.
- Analizador del cuerpo y registro
- Estáticos
- Routers de la API
- 404 para lo que no encajó
- 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
- Explora dos APIs públicas y anota diez rutas de cada una.
- Clasifícalas: ¿orientadas a recursos o a acciones?
- Localiza cómo filtran, cómo paginan y cómo devuelven los errores.
- Diseña sobre papel la tabla de rutas de tu proyecto: método, ruta, qué hace, qué devuelve y con qué código.
- 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
- Escribe la tabla completa de tu API, con éxitos y errores por ruta.
- Define el objeto que representa un producto: campos, tipos y cuáles son obligatorios.
- Define el formato único de error, con sus códigos.
- Añade paginación al contrato y decide qué devuelve exactamente la respuesta paginada.
- Escribe
peticiones.httpcon 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
- Crea
src/app.jsconcrearApp()y dejaservidor.jssolo con el arranque. - Mueve las rutas de productos a su router.
- Mueve el registro y los errores a
src/middleware/. - Comprueba que todo sigue funcionando con tu fichero de peticiones.
- Añade el router del segundo recurso, aunque esté vacío, y monta su ruta base.
- Monta la API bajo
/api/v1cambiando 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
- Escribe un pequeño programa que importe
crearApp()y le haga una petición sin llamar alisten. - Comprueba que ningún puerto queda ocupado: arráncalo dos veces seguidas y mira que no da
EADDRINUSE. - Ahora haz lo contrario: mete el
listendentro deapp.jsy repite el apartado 1. Anota qué ocurre. - Devuelve el
listena su sitio. - 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.
- Enumera todo lo que se rompería en un cliente que ya funciona.
- 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.
- Implementa la que elijas de modo que
/api/v1/productossiga devolviendo lo de antes y/api/v2/productosdevuelva la forma nueva. Reutiliza la lógica: lo que cambia es la representación, no los datos. - ¿Durante cuánto tiempo mantendrías la v1, y cómo avisarías de que va a desaparecer? Busca qué cabecera existe para eso.
- Hay cambios que no rompen el contrato y se pueden hacer sin versionar. Escribe tres ejemplos y explica por qué son seguros.
- 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.
- Documéntala: qué hace, con qué rutas, y qué te parece discutible.
- 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.
- Escribe cómo la harías tú, con la tabla de rutas equivalente.
- 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.
- Busca en la misma API una decisión que al principio te pareciera rara y que, al mirarla despacio, resulte acertada. Explícala.
- Compara esa API con tu contrato: ¿qué le copiarías?
/api/v1 cambiando una línea.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
- ¿Por qué las rutas no llevan verbos, y dónde van los filtros?
- ¿Qué significa que el servidor no guarde estado?
- ¿Qué código devuelve una creación correcta, y qué cabecera la acompaña?
- ¿Qué responde
/api/productos/abc, y por qué no un 404? - ¿Qué operaciones se pueden repetir sin consecuencias, y cuál no?
- ¿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.
- Reescribe orientadas a recursos:
/obtenerProductosy/borrarProducto?id=7. - Di qué código de estado devuelve cada operación del CRUD cuando va bien.
- ¿Por qué la aplicación se crea en un fichero y se arranca en otro?