📚 Unidad ISS-05 · Swagger / OpenAPI — capa 🧠 APRENDER
🛠 Construir este bloque → · ✅ Condiciones de cierre (GATE) · 📝 Evaluación
Capa Página Para qué 🧠 Aprender esta página comprender, explicar y relacionar 🛠 Construir Swagger / OpenAPI ejecutar, programar y verificar ✅ GATE Cierre de la unidad condición para pasar al bloque siguiente
Mapa de correspondencias. Cada fila enlaza el mismo tema en las dos capas de la unidad; los enlaces apuntan a secciones reales del material (anclas de MkDocs, sin acentos).
| Tema | 🧠 Aprender (esta página) | 🛠 Construir (ISS técnico) |
|---|---|---|
| Ruta y ficha de la unidad | Ruta de aprendizaje · Ficha del ISS | Contenido de la unidad |
| Mapas y estructura | Mapa mental · Mapa del backend · Árbol de archivos | Contenido de la unidad |
| Recorrido y comandos | Comandos explicados · Recorrido paso a paso | Contenido de la unidad |
| Diagnóstico | Diagnóstico | Criterios de aceptación |
| Evaluación y cierre | Criterios · Evaluación · GATE | Condiciones de cierre · Cierre |
🖥 Presentación del ISS
Presentación de la unidad. Diapositivas de ISS-05 — Swagger / OpenAPI (8 diapositivas). Se visualiza aquí, dentro del sitio; no hace falta descargar nada para estudiarla.
🎬 Video explicativo
Recorrido audiovisual de la unidad. Este video explica, bloque por bloque, el Swagger de clientes del ISS-05: el módulo y el registry, los paths y los schemas,
setupSwagger, y la diferencia entre el comentario SIN AUTH y elbearerSecurityque documenta la operación.
8:33 · narración en español · subtítulos activables desde el reproductor.
ISS-05 — Cuaderno de aprendizaje visual
Tema
Swagger / OpenAPI: documentar el API del feature clients en OpenAPI 3 y montar Swagger UI desde un registry externo (mismo patrón que los seeders).
Fuente técnica autoritativa
| Archivo fuente | ../manual/06-ISS-05-swagger.md |
| Estado | Solo lectura — este cuaderno no modifica el ISS |
| Alcance | Documento OpenAPI del feature clients (clients.swagger.ts), registry externo (src/swagger/index.ts) y el montaje en src/config/index.ts bajo /api/docs y /api/docs.json |
El ISS manda; el cuaderno explica. Todo el contenido técnico del ISS (paquetes, rutas, versiones y criterios) aparece aquí íntegro y verbatim más abajo, en la sección Recorrido del ISS, paso a paso. Lo único que añade este cuaderno es el por qué.
Pregunta que responde: ¿de dónde sale cada dato técnico de este cuaderno?
Regla del ISS
Objetivo: documentar el API del feature Client en OpenAPI 3 y montar Swagger UI desde un registry externo (mismo patrón que seeders). Bloqueado por: ISS-03-E (rutas CRUD definidas) — en ISS-03.
La condición que el propio ISS exige es observable con dos URLs: GET /api/docs debe mostrar Swagger UI y GET /api/docs.json debe devolver el documento OpenAPI. Además, el montaje no puede hacerse dentro del feature: la App lo invoca mediante el método docs(), que llama a setupSwagger(app).
Pregunta que responde: ¿qué tiene que pasar para poder dar este ISS por bueno?
Cómo leer este cuaderno
Cada concepto se presenta tres veces, desde tres ángulos distintos:
CONCEPTO
│
┌───────────┼───────────┐
▼ ▼ ▼
EXPLICACIÓN CÓDIGO VISUAL
│ │ │
¿qué es? ¿dónde está? ¿cómo lo
¿por qué? ¿qué hace? visualizo?
¿para qué? ¿cómo opera? ¿con qué
se relaciona?
Pregunta que responde: ¿cómo está organizado este cuaderno y por qué se enseña todo tres veces?
El recorrido de lectura es siempre el mismo:
Pregunta que responde: ¿en qué orden recorro cada concepto dentro del cuaderno?
Y cada cuaderno contiene los mismos seis componentes:
| Componente | Dónde vive | Para qué sirve |
|---|---|---|
| Texto | todas las secciones | entender el por qué |
| Código | Recorrido del ISS |
ver el qué exacto (verbatim del ISS) |
| Diagramas | Mapa mental, Mapa del backend, Flujos |
ver el cómo se conecta |
| Preguntas | Evaluación |
comprobar que entendiste |
| Evaluación | Evaluación |
practicar y autoevaluarte |
| GATE | GATE |
saber si puedes pasar al siguiente ISS |
Ruta de aprendizaje
Esta ruta es específica de ISS-05: primero el módulo del feature, después el registry y al final el montaje en la App.
Instalar swagger-ui-express y sus tipos
↓
Escribir clients.swagger.ts (clientsSwagger)
↓
Escribir src/swagger/index.ts (registry + setupSwagger)
↓
Parchear src/config/index.ts (import + this.docs() + docs())
↓
Arrancar el servidor
↓
Abrir /api/docs y /api/docs.json
↓
Verificar (GATE)
Pregunta que responde: ¿cuál es el camino concreto que sigo para completar este ISS?
Índice
- Ficha del ISS
- Mapa mental del ISS
- Mapa del backend
- Árbol de archivos
- Anatomía del código
- Flujos
- Comandos explicados
- Recorrido del ISS, paso a paso
- Diagnóstico
- Conexión con el resto del curso
- Criterios de aceptación
- Evaluación
- Glosario
- GATE
Ficha del ISS
| Campo | Valor |
|---|---|
| ISS | ISS-05 |
| Título | Swagger / OpenAPI (feature + registry) |
| Objetivo | Documentar el API del feature Client en OpenAPI 3 y montar Swagger UI desde un registry externo |
| Fase | Fase I — Business |
| Tecnología principal | swagger-ui-express ^5.0.1 + OpenAPI 3.0.3 |
| Depende de | ISS-03 — Feature Client (CRUD por capas) (ISS-03-E rutas CRUD) |
| Habilita | ISS-06 — Feature ProductType |
| Archivos creados | src/features/business/clients/clients.swagger.ts, src/swagger/index.ts |
| Archivos parcheados | src/config/index.ts |
| Componentes incorporados | Módulo OpenAPI del feature + registry buildOpenApiDocument/setupSwagger + montaje App.docs() |
| Verificación principal | GET /api/docs muestra Swagger UI y GET /api/docs.json devuelve el OpenAPI |
| Resultado esperado | Documento OpenAPI 3.0.3 con tags, paths y schemas de Client visible en el navegador |
| GATE | curl ... /api/docs.json responde y el servidor arranca sin error |
Qué implementamos AHORA
- Un módulo OpenAPI por feature:
clientsSwaggerexportatags,pathsycomponents.schemasdeClient. - Un registry externo (
src/swagger/index.ts) que fusiona los módulos de features en un documento único conbuildOpenApiDocument(). - El montaje de la UI y del JSON con
setupSwagger(app). - El enganche en la App:
src/config/index.tsimportasetupSwaggery lo invoca desde el métododocs(). - Las dependencias
swagger-ui-express(prod) y@types/swagger-ui-express(dev).
Qué todavía NO implementamos
| No se implementa aquí | Llega en |
|---|---|
Segunda feature (product-types) |
ISS-06 |
products y asociaciones |
ISS-07 |
sales y product-sales |
ISS-08 |
Módulos Swagger de features de auth (users, session, …) |
ISS-09 … ISS-15 |
La seguridad real de las rutas (middlewares authenticate / authorize) |
ISS-09 … ISS-13 |
Ojo con la trampa habitual: Swagger documenta el contrato, no lo protege. Que una ruta aparezca en
/api/docsconsecurityno significa que todavía exista el middleware que la valide.
Mapa mental del ISS
mindmap
root((ISS-05<br/>Swagger OpenAPI))
Objetivo
Documentar feature Client
Montar Swagger UI
Modulo del feature
clients.swagger.ts
tags
paths
schemas
Registry externo
src swagger index.ts
buildOpenApiDocument
setupSwagger
Montaje en la App
config docs
constructor
Rutas
UI en api docs
Spec en api docs json
Contrato
openapi 3.0.3
StoreLab API 1.0.0
GATE
curl spec
abrir UI
Pregunta que responde: ¿de qué trata este ISS y qué piezas lo componen?
Mapa del backend
Esta representación responde siempre a la misma pregunta: ¿en qué parte del backend estamos?
IMPLEMENTADO HASTA ESTE ISS
───────────────────────────
HTTP → Express App ✅
↓
Routes → Controller → Service → Repository → Model → DTO ✅ (feature clients, ISS-03)
↓
Sequelize → BD ✅
Seeders (ISS-04) ✅
🆕 Documentación OpenAPI (fuera del recorrido HTTP) ✅ (ISS-05)
├── features/business/clients/clients.swagger.ts (clientsSwagger)
├── src/swagger/index.ts (buildOpenApiDocument, setupSwagger)
└── App.docs() → setupSwagger(app)
Rutas de documentación
├── GET /api/docs → Swagger UI ✅
└── GET /api/docs.json → documento OpenAPI ✅
OBJETIVO DE ARQUITECTURA
────────────────────────
🎯 + feature product-types y su módulo Swagger (ISS-06)
🎯 + feature products y asociaciones (ISS-07)
🎯 + sales y product-sales (ISS-08)
🎯 + capa de seguridad, RBAC y sesión (ISS-09 … ISS-15)
⬜ Nada del stack de autenticación está construido todavía
Pregunta que responde: ¿qué capas del backend existen ya y cuáles siguen siendo objetivo?
Al igual que los seeders, Swagger se añade al lado de la cadena de capas: es un registry que consume los módulos de los features, no una capa más del recorrido de una petición de negocio.
Árbol de archivos
Leyenda: ★ = creado o trabajado en este ISS · △ = existente parcheado en este ISS.
Estructura antes
src/
├── config/
│ └── index.ts
├── database/
│ ├── db.ts
│ └── seeders/
│ ├── counts.ts
│ └── index.ts
├── features/
│ └── business/
│ └── clients/
│ ├── client.model.ts
│ ├── clients.controller.ts
│ ├── clients.repository.ts
│ ├── clients.routes.ts
│ ├── clients.service.ts
│ ├── clients.seeder.ts
│ └── dto/
├── routes/
│ └── index.ts
├── shared/
│ └── http/
│ └── swagger-security.ts (bearerSecurity, …)
├── server.ts
└── swagger/ ← todavía no existe
package.json
Pregunta que responde: ¿qué archivos existían ya antes de empezar este ISS?
Archivos creados / modificados en este ISS
★ src/features/business/clients/clients.swagger.ts módulo OpenAPI del feature
★ src/swagger/index.ts registry externo + montaje
△ src/config/index.ts import + this.docs() + método docs()
Pregunta que responde: ¿qué toca exactamente este ISS y con qué rol?
Estructura después
src/
├── config/
│ └── index.ts △ (método docs() añadido)
├── database/
│ ├── db.ts
│ └── seeders/
│ ├── counts.ts
│ └── index.ts
├── features/
│ └── business/
│ └── clients/
│ ├── client.model.ts
│ ├── clients.controller.ts
│ ├── clients.repository.ts
│ ├── clients.routes.ts
│ ├── clients.service.ts
│ ├── clients.seeder.ts
│ ├── clients.swagger.ts ★
│ └── dto/
├── routes/
│ └── index.ts
├── shared/
│ └── http/
│ └── swagger-security.ts
├── server.ts
└── swagger/ ★ (nuevo)
└── index.ts ★
package.json (dependencias swagger-ui-express añadidas)
Pregunta que responde: ¿cómo queda el proyecto después de este ISS?
Anatomía del código
Archivo: src/features/business/clients/clients.swagger.ts
Propósito
Declarar el contrato OpenAPI del feature clients: la etiqueta de la UI, las rutas (paths) y los esquemas (components.schemas). Exporta el objeto clientsSwagger.
Explicación
- Imports de seguridad — importa
bearerSecurity,forbiddenResponseyunauthorizedResponsedesdeshared/http/swagger-security; son los fragmentos reutilizables que declaran elsecurityy las respuestas401/403. tags— un tagClientesque la UI usa para agrupar los endpoints.paths— documenta/api/clientes(GET/POST) y/api/clientes/{id}(GET/PUT/PATCH/DELETE), más/api/clientes/{id}/deactivate(PATCH). Cada operación llevatags,summary,description,securityyresponses.- Parámetro
id— se declara como enterominimum: 1y con sus respuestas400(id inválido) y404. components.schemas— cuatro esquemas:Client,ClientCreate,ClientUpdateyClientPatch, con ejemplos yrequireddonde corresponde (la contraseña nunca forma parte deClient).
Se conecta con
- Entrada: lo importa el registry
src/swagger/index.ts. - Salida: no llama a nada en ejecución; es un objeto de datos puro.
Nota didáctica del propio ISS: el criterio 10.1 habla de la leyenda SIN AUTH, mientras que el módulo declara
security: bearerSecurityy respuestas401/403. Aquí se reproducen ambos tal cual: el criterio, textual en Criterios de aceptación, y el código, verbatim en el Recorrido.
Archivo: src/swagger/index.ts
Propósito
Ser el registry externo: reunir los módulos OpenAPI de cada feature, fusionarlos en un documento único y montar la UI y el JSON en la App.
Explicación
- Tipo
FeatureSwaggerModule— contrato mínimo que debe cumplir un módulo de feature:tags,pathsy, opcionalmente,components.schemas. featureSwaggerModules— arreglo con los módulos importados (clientsSwaggery, comentados, los futurosproductsSwagger,userSwagger). Añadir una entidad es añadir una línea aquí.buildOpenApiDocument()— recorre los módulos acumulandotags,pathsyschemas, y devuelve el documento conopenapi: "3.0.3",info(StoreLab API,1.0.0),servers(usandoprocess.env.PORTo4000) ycomponents.setupSwagger(app)— construye el documento, montaswaggerUi.serve+swaggerUi.setup(document)en/api/docsy exponeGET /api/docs.json, además de loguear las dos rutas.
Se conecta con
- Entrada: lo llama
App.docs()ensrc/config/index.ts. - Salida: importa
clientsSwagger; usaswagger-ui-express.
Archivo: src/config/index.ts (parche)
Propósito
Enganchar el montaje de la documentación en el ciclo de vida de la App, sin tocar el feature.
Explicación
- Import — añade
import { setupSwagger } from "../swagger/index";debajo de los imports de rutas/BD. - Constructor — añade
this.docs();debajo dethis.routes();, de modo que la documentación se monte al arrancar. - Método
docs()— método privado que simplemente delega ensetupSwagger(this.app), situado debajo deroutes()y encima dedbConnection().
Se conecta con
- Entrada: lo ejecuta el constructor de la App al arrancar el servidor.
- Salida: llama a
setupSwagger.
Flujos
sequenceDiagram
autonumber
participant Dev as "Desarrollador"
participant App as "App config"
participant Reg as "registry src/swagger/index.ts"
participant UI as "swagger-ui-express"
participant Cli as "clientsSwagger"
Dev->>App: npm run dev
App->>App: constructor llama this.docs()
App->>Reg: setupSwagger(app)
Reg->>Cli: importa clientsSwagger
Reg->>Reg: buildOpenApiDocument()
Reg->>UI: app.use /api/docs serve setup
Reg->>App: app.get /api/docs.json
Dev->>Reg: GET /api/docs.json
Reg-->>Dev: documento OpenAPI 3.0.3
Pregunta que responde: ¿qué ocurre desde que arranca el servidor hasta que veo la documentación?
flowchart TD
A["clientsSwagger"] --> D["buildOpenApiDocument()"]
B["productsSwagger (futuro)"] -.-> D
C["userSwagger (futuro)"] -.-> D
D --> E["tags + paths + schemas"]
E --> F["Swagger UI en /api/docs"]
E --> G["OpenAPI JSON en /api/docs.json"]
Pregunta que responde: ¿cómo se fusionan los módulos de los features en un solo documento?
Comandos explicados
npm install swagger-ui-express@^5.0.1
COMANDO
↓
npm install swagger-ui-express@^5.0.1
↓
QUÉ HACE
Instala el paquete que sirve la interfaz de Swagger UI sobre Express.
↓
POR QUÉ SE NECESITA
Es la pieza que se monta en /api/docs; va en dependencias normales
porque la App la usa en ejecución.
↓
QUÉ CREA O MODIFICA
package.json (dependencies) y package-lock.json.
↓
RESULTADO ESPERADO
La versión ^5.0.1 registrada.
↓
CÓMO VERIFICARLO
npm ls swagger-ui-express.
npm install -D @types/swagger-ui-express@^4.1.8
COMANDO
↓
npm install -D @types/swagger-ui-express@^4.1.8
↓
QUÉ HACE
Instala los tipos TypeScript del paquete anterior.
↓
POR QUÉ SE NECESITA
Sin ellos, el import de swagger-ui-express no compilaría con TypeScript.
↓
QUÉ CREA O MODIFICA
package.json (devDependencies) y package-lock.json.
↓
RESULTADO ESPERADO
La versión ^4.1.8 registrada.
↓
CÓMO VERIFICARLO
npm ls @types/swagger-ui-express.
mkdir -p src/swagger
COMANDO
↓
mkdir -p src/swagger
↓
QUÉ HACE
Crea la carpeta del registry externo.
↓
POR QUÉ SE NECESITA
El archivo src/swagger/index.ts debe tener dónde vivir.
↓
QUÉ CREA O MODIFICA
El directorio src/swagger/.
↓
RESULTADO ESPERADO
La carpeta existe (sin error si ya estaba).
↓
CÓMO VERIFICARLO
ls src/swagger.
curl -s http://localhost:4000/api/docs.json | head
COMANDO
↓
curl -s http://localhost:4000/api/docs.json | head
↓
QUÉ HACE
Pide el documento OpenAPI sin ruido de progreso y muestra el inicio.
↓
POR QUÉ SE NECESITA
Es la verificación por consola de que la spec se está sirviendo.
↓
QUÉ CREA O MODIFICA
Nada.
↓
RESULTADO ESPERADO
Las primeras líneas del JSON (openapi, info, servers…).
↓
CÓMO VERIFICARLO
Si ves `"openapi": "3.0.3"`, el criterio pasa.
npm run dev
COMANDO
↓
npm run dev
↓
QUÉ HACE
Arranca el servidor Express en modo desarrollo con nodemon.
↓
POR QUÉ SE NECESITA
Es el cierre del ISS: sin servidor no hay /api/docs que abrir.
↓
QUÉ CREA O MODIFICA
Nada persistente: levanta el proceso.
↓
RESULTADO ESPERADO
El log del montaje: "📘 Swagger UI: /api/docs | OpenAPI JSON: /api/docs.json".
↓
CÓMO VERIFICARLO
Abrir http://localhost:4000/api/docs y detener con Ctrl+C.
Recorrido del ISS, paso a paso
A partir de aquí viene el contenido técnico completo del ISS, verbatim: su objetivo, sus criterios y sus bloques de código. Se reproduce sin resumir y sin reformatear; solo se han degradado los encabezados un nivel para que aniden bajo esta sección, y se han reescrito los enlaces relativos hacia ../manual/.
Fase I: Business — ISS-05 — Swagger / OpenAPI (feature + registry)
Contexto mínimo (autosuficiente). Si abres este archivo aislado —o lo llevas a otra IA—, esto es lo que hay que saber antes de ejecutarlo. - Proyecto:
app-storelab-express-ii— Express 5 + TypeScript + Sequelize, arquitectura por features, Fase I solo Business (sin auth ni roles). - Recorrido obligatorio de una petición:HTTP → Controller → Service → Repository → Model → Sequelize → BD. Ninguna capa se salta a la siguiente. - Diseño de datos (columnas, tipos, FKRESTRICT, índices, transacciones):../bd-storelab.md, Fase I — Business. - Capas, convenciones y reglas transversales:00-contexto.md.
Este ISS Título Swagger / OpenAPI (feature + registry) Feature / tabla clients(swagger)API /api/docs,/api/docs.jsonDepende de ISS-03 — Feature Client (CRUD por capas) Habilita ISS-06 — Feature ProductType
Contenido de este ISS
- 10.1 OpenAPI dentro del feature Client
- 10.2 Registry externo + montaje en Config
Objetivo: documentar el API del feature Client en OpenAPI 3 y montar Swagger UI desde un registry externo (mismo patrón que seeders).
Bloqueado por: ISS-03-E (rutas CRUD definidas) — en ISS-03.
Criterios de aceptación (ISS-05) — consolidados
- [ ] 10.1 Existe
features/business/clients/clients.swagger.tscon tags, paths y schemas de Client (leyenda SIN AUTH) - [ ] 10.2 Existe
src/swagger/index.tsque agrega módulos de features y monta UI - [ ]
AppllamasetupSwagger(métododocs()) - [ ]
GET /api/docsmuestra Swagger UI - [ ]
GET /api/docs.jsondevuelve el documento OpenAPI
Diseño
| Pieza | Ubicación | Rol |
|---|---|---|
| Docs del feature | src/features/business/clients/clients.swagger.ts |
Paths + schemas Client |
| Registry | src/swagger/index.ts |
Fusiona features + setupSwagger(app) |
| UI | /api/docs |
Swagger UI |
| Spec | /api/docs.json |
OpenAPI JSON |
10.1 OpenAPI dentro del feature Client
Criterios
- [ ] Exporta
clientsSwaggercontags,paths,components.schemas - [ ] Endpoints documentados como SIN AUTH
# Paquetes (una vez)
npm install swagger-ui-express@^5.0.1
npm install -D @types/swagger-ui-express@^4.1.8
Archivo nuevo:
: > src/features/business/clients/clients.swagger.ts
cat >> src/features/business/clients/clients.swagger.ts << 'EOF'
import {
bearerSecurity,
forbiddenResponse,
unauthorizedResponse,
} from "../../../shared/http/swagger-security";
/**
* Documentación OpenAPI del feature Client.
* Se agrega desde `src/swagger` (registry externo), no se monta aquí.
*
* Leyenda: todos los endpoints son **JWT + RBAC** (`authenticate` + `authorize`).
* La modalidad se declara por operación: aquí heredan el `security` global del documento.
*/
export const clientsSwagger = {
tags: [
{
name: "Clientes",
description: "CRUD de clientes — **JWT + RBAC** (authenticate + authorize)",
},
],
paths: {
"/api/clientes": {
get: {
tags: ["Clientes"],
summary: "Listar clientes activos",
description: "JWT + RBAC — retorna clientes con status=active (sin password)",
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"200": {
description: "Lista de clientes",
content: {
"application/json": {
schema: {
type: "object",
properties: {
clients: {
type: "array",
items: { $ref: "#/components/schemas/Client" },
},
},
},
},
},
},
},
},
post: {
tags: ["Clientes"],
summary: "Crear cliente",
description: "JWT + RBAC",
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ClientCreate" },
},
},
},
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"201": {
description: "Cliente creado",
content: {
"application/json": {
schema: {
type: "object",
properties: {
client: { $ref: "#/components/schemas/Client" },
},
},
},
},
},
},
},
},
"/api/clientes/{id}": {
get: {
tags: ["Clientes"],
summary: "Obtener cliente por id",
description: "JWT + RBAC — 404 si no existe o tiene borrado lógico",
parameters: [
{
name: "id",
in: "path",
required: true,
description: "id numérico (> 0). Si no lo es, la API responde 400.",
schema: { type: "integer", minimum: 1 },
},
],
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"200": {
description: "Cliente encontrado",
content: {
"application/json": {
schema: {
type: "object",
properties: {
client: { $ref: "#/components/schemas/Client" },
},
},
},
},
},
"400": { description: "id inválido (debe ser un entero positivo)" },
"404": { description: "No encontrado" },
},
},
put: {
tags: ["Clientes"],
summary: "Actualizar cliente (PUT — reemplazo)",
description: "JWT + RBAC",
parameters: [
{
name: "id",
in: "path",
required: true,
description: "id numérico (> 0). Si no lo es, la API responde 400.",
schema: { type: "integer", minimum: 1 },
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ClientUpdate" },
},
},
},
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"200": { description: "Actualizado" },
"400": { description: "id inválido (debe ser un entero positivo)" },
"404": { description: "No encontrado" },
},
},
patch: {
tags: ["Clientes"],
summary: "Actualizar cliente (PATCH — parcial)",
description: "JWT + RBAC",
parameters: [
{
name: "id",
in: "path",
required: true,
description: "id numérico (> 0). Si no lo es, la API responde 400.",
schema: { type: "integer", minimum: 1 },
},
],
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ClientPatch" },
},
},
},
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"200": { description: "Actualizado" },
"400": { description: "id inválido (debe ser un entero positivo)" },
"404": { description: "No encontrado" },
},
},
delete: {
tags: ["Clientes"],
summary: "Eliminar cliente (físico)",
description: "JWT + RBAC — borra la fila (también si tiene borrado lógico)",
parameters: [
{
name: "id",
in: "path",
required: true,
description: "id numérico (> 0). Si no lo es, la API responde 400.",
schema: { type: "integer", minimum: 1 },
},
],
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"200": { description: "Eliminado" },
"400": { description: "id inválido (debe ser un entero positivo)" },
"404": { description: "No encontrado" },
},
},
},
"/api/clientes/{id}/deactivate": {
patch: {
tags: ["Clientes"],
summary: "Eliminar cliente (lógico)",
description: "JWT + RBAC — status = inactive",
parameters: [
{
name: "id",
in: "path",
required: true,
description: "id numérico (> 0). Si no lo es, la API responde 400.",
schema: { type: "integer", minimum: 1 },
},
],
security: bearerSecurity,
responses: {
"401": unauthorizedResponse,
"403": forbiddenResponse,
"200": { description: "Desactivado" },
"400": { description: "id inválido (debe ser un entero positivo)" },
"404": { description: "No encontrado" },
},
},
},
},
components: {
schemas: {
Client: {
type: "object",
properties: {
id: { type: "integer", example: 1 },
name: { type: "string", example: "Ana Pérez" },
address: { type: "string", example: "Calle 10 #20-30" },
phone: { type: "string", example: "3001234567" },
email: { type: "string", format: "email", example: "ana@example.com" },
status: { type: "string", enum: ["active", "inactive"], example: "active" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
ClientCreate: {
type: "object",
required: ["name", "phone", "email", "password"],
properties: {
name: { type: "string" },
address: { type: "string" },
phone: { type: "string" },
email: { type: "string", format: "email" },
password: { type: "string", format: "password" },
status: { type: "string", enum: ["active", "inactive"], default: "active" },
},
},
ClientUpdate: {
type: "object",
required: ["name", "phone", "email"],
properties: {
name: { type: "string" },
address: { type: "string" },
phone: { type: "string" },
email: { type: "string", format: "email" },
password: { type: "string", format: "password" },
},
},
ClientPatch: {
type: "object",
properties: {
name: { type: "string" },
address: { type: "string" },
phone: { type: "string" },
email: { type: "string", format: "email" },
password: { type: "string", format: "password" },
},
},
},
},
};
EOF
10.2 Registry externo + montaje en Config
Criterios
- [ ]
buildOpenApiDocument()fusiona módulos de features - [ ]
setupSwagger(app)monta/api/docsy/api/docs.json - [ ]
configinvocasetupSwagger(métododocs())
Archivo nuevo:
: > src/swagger/index.ts
cat >> src/swagger/index.ts << 'EOF'
import { Application } from "express";
import swaggerUi from "swagger-ui-express";
import { clientsSwagger } from "../features/business/clients/clients.swagger";
export type FeatureSwaggerModule = {
tags: unknown[];
paths: Record<string, unknown>;
components?: { schemas?: Record<string, unknown> };
};
/**
* Registry externo: importa la documentación OpenAPI de cada feature
* (mismo patrón que SeedersRunner).
*/
const featureSwaggerModules: FeatureSwaggerModule[] = [
clientsSwagger,
// productsSwagger,
// userSwagger,
];
export function buildOpenApiDocument() {
const tags: unknown[] = [];
const paths: Record<string, unknown> = {};
const schemas: Record<string, unknown> = {};
for (const mod of featureSwaggerModules) {
tags.push(...mod.tags);
Object.assign(paths, mod.paths);
if (mod.components?.schemas) {
Object.assign(schemas, mod.components.schemas);
}
}
return {
openapi: "3.0.3",
info: {
title: "StoreLab API",
version: "1.0.0",
description:
"API StoreLab (Express + Sequelize). Los endpoints de Client están documentados como **SIN AUTH** Todas las rutas business son **SIN AUTH** en este lab.",
},
servers: [
{
url: `http://localhost:${process.env.PORT || 4000}`,
description: "Local",
},
],
tags,
paths,
components: { schemas },
};
}
/** Monta Swagger UI y el JSON OpenAPI */
export function setupSwagger(app: Application): void {
const document = buildOpenApiDocument();
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(document));
app.get("/api/docs.json", (_req, res) => {
res.json(document);
});
console.log("📘 Swagger UI: /api/docs | OpenAPI JSON: /api/docs.json");
}
EOF
PARCHE — src/config/index.ts ya existe.
- Debajo de
import { Routes } from "../routes/index";(o debajo de los imports de BD/modelo), añadir:
- Dentro del
constructor, debajo dethis.routes();, añadir:
- Dentro de la clase
App, debajo de el métodoroutes()y encima dedbConnection(), añadir:
Verificación ISS-05
Con el servidor del cierre: abrir
http://localhost:4000/api/docs.
Al agregar otra entidad (patrón):
- Archivo nuevo
features/.../<plural>.swagger.tscon: >+cat >>. - PARCHE
src/swagger/index.ts: debajo deimport { clientsSwagger } ..., añadir el import; dentro defeatureSwaggerModules, debajo declientsSwagger,, añadir el módulo nuevo.
Cierre del ISS
El servidor debe arrancar sin error. Abrir
http://localhost:4000/api/docs. Detenerlo con Ctrl+C antes de continuar.
Diagnóstico
| Síntoma | Causa probable | Solución |
|---|---|---|
GET /api/docs devuelve 404 |
App.docs() no se llama en el constructor |
Comprobar el parche de src/config/index.ts (import + this.docs() + método docs()) |
Cannot find module 'swagger-ui-express' |
Falta la dependencia o los tipos | Instalar swagger-ui-express@^5.0.1 y @types/swagger-ui-express@^4.1.8 |
| El documento sale sin paths | El módulo del feature no está en featureSwaggerModules |
Añadir el import y la línea del módulo en src/swagger/index.ts |
TS2307 al importar swagger-ui-express |
Tipos no instalados en dev | npm install -D @types/swagger-ui-express@^4.1.8 |
La UI carga pero no hay endpoints de Clientes |
El tag/objeto clientsSwagger no exporta paths |
Revisar el módulo del feature; el registry solo fusiona lo que exporta |
| Los endpoints se ven sin candado | security no declarado en las operaciones |
Es lo que define el ISS en el módulo; revisar bearerSecurity |
Pregunta que responde: si la documentación no aparece, ¿por dónde empiezo a mirar?
Conexión con el resto del curso
- Lo habilita: ISS-03 — Feature Client. Sin rutas CRUD no hay nada que documentar.
- Reutiliza un patrón: el registry externo que ya viste en ISS-04 con el
SeedersRunner: la pieza que orquesta vive fuera del feature. - Lo usan: todos los ISS siguientes, cada uno añadiendo su módulo
.swagger.tsafeatureSwaggerModules. - Diferencia clave: un seeder escribe en la BD; el registry de Swagger no toca datos: solo describe el contrato.
Pregunta que responde: ¿con qué piezas anteriores y posteriores del curso se conecta este ISS?
Criterios de aceptación
Los del ISS, textuales:
- [ ] 10.1 Existe
features/business/clients/clients.swagger.tscon tags, paths y schemas de Client (leyenda SIN AUTH) - [ ] 10.2 Existe
src/swagger/index.tsque agrega módulos de features y monta UI - [ ]
AppllamasetupSwagger(métododocs()) - [ ]
GET /api/docsmuestra Swagger UI - [ ]
GET /api/docs.jsondevuelve el documento OpenAPI
Criterios internos de cada bloque (también del ISS):
- [ ] Exporta
clientsSwaggercontags,paths,components.schemas - [ ] Endpoints documentados como SIN AUTH
- [ ]
buildOpenApiDocument()fusiona módulos de features - [ ]
setupSwagger(app)monta/api/docsy/api/docs.json - [ ]
configinvocasetupSwagger(métododocs())
Evaluación
Preguntas de comprensión
-
¿Por qué el registry es «externo» y no se monta dentro del feature? Porque un feature no debería saber cómo se sirve la documentación de toda la App. El módulo del feature solo declara su contrato (
clientsSwagger);src/swagger/index.tslo consume y decide cómo fusionarlo y montarlo. Es el mismo principio que separa el seeder delSeedersRunner. -
¿Qué devuelve
buildOpenApiDocument()y para qué sirve? Devuelve un objeto OpenAPI 3.0.3 coninfo,servers,tags,pathsycomponents.schemas, fusionando todos los módulos de features. Sirve tanto para la UI (swaggerUi.setup) como para el JSON de/api/docs.json. -
¿Para qué existen dos rutas,
/api/docsy/api/docs.json?/api/docssirve la interfaz visual (Swagger UI) para humanos;/api/docs.jsonsirve el documento crudo para herramientas (clientes, pruebas, validadores). La misma spec, dos formatos. -
¿Qué papel juega
configy por qué no basta con crear el registry? Porque el registry es código que nadie ejecuta hasta que alguien lo llame.configenganchasetupSwaggeral arranque mediantethis.docs(); sin ese enganche el archivo existe pero la documentación no se monta. -
El módulo importa
bearerSecurity,forbiddenResponseyunauthorizedResponse. ¿Qué aporta eso al documento? Son fragmentos reutilizables deshared/http/swagger-securityque declaran, por operación, el esquema de seguridad (security) y las respuestas normalizadas401y403. Evitan repetir esos bloques en cada endpoint. -
¿Cómo añadirías la documentación de una entidad nueva sin tocar el feature existente? Creando
features/.../<plural>.swagger.tsque exporte su módulo, e importándolo/añadiéndolo enfeatureSwaggerModulesdesrc/swagger/index.ts. El patrón está descrito al final del ISS.
Ejercicios
Ejercicio 1 — Sigue el rastro del documento.
Enumera, en orden, las llamadas que llevan desde npm run dev hasta que el navegador recibe el JSON en /api/docs.json.
Respuesta razonada
1. `App` se construye y el constructor ejecuta `this.docs()`. 2. `docs()` llama a `setupSwagger(this.app)`. 3. `setupSwagger` llama a `buildOpenApiDocument()`. 4. `buildOpenApiDocument` recorre `featureSwaggerModules` (aquí, `clientsSwagger`) y fusiona `tags`, `paths` y `schemas`. 5. `setupSwagger` registra `GET /api/docs.json`, que responde con ese documento cuando el navegador lo pide.Ejercicio 2 — Un módulo que no aparece. Documentas un endpoint, pero en la UI no sale. Nombra dos causas probables y cómo descartarlas.
Respuesta razonada
- El módulo del feature **no está** en `featureSwaggerModules` (falta el import o la línea). Se descarta revisando ese arreglo y ejecutando `curl /api/docs.json` para ver si el path aparece en el JSON. - El módulo está, pero **no exporta `paths`** (o el nombre exportado es distinto). Se descarta comprobando que el objeto exportado tenga las claves `tags`/`paths` que espera `FeatureSwaggerModule`.Ejercicio 3 — Documentar no es proteger.
Explica por qué ver un candado (o un security) en Swagger UI no garantiza que la ruta esté protegida en este punto del curso.
Respuesta razonada
Porque Swagger solo **describe** el contrato: es un objeto de datos que dice «se espera este token», pero no ejecuta ninguna validación. Quien protege una ruta son los middlewares (`authenticate`/`authorize`) montados en las rutas reales, que llegan en ISS-09 … ISS-13. Hasta entonces, la documentación puede anticipar algo que el código todavía no aplica.Glosario
| Término | Significado |
|---|---|
| OpenAPI | Estándar para describir APIs REST de forma legible por máquinas (aquí, 3.0.3). |
| Swagger UI | Interfaz web que renderiza un documento OpenAPI y permite probarlo. |
| Registry | Archivo que reúne los módulos de features y produce un documento único. |
paths |
Sección del documento que describe rutas y operaciones HTTP. |
schemas |
Definiciones reutilizables de los cuerpos de petición y respuesta. |
tags |
Agrupaciones de la UI para organizar endpoints. |
security |
Declaración del esquema de autenticación de una operación. |
GATE
Para cerrar el ISS-05, arranca el servidor:
Resultado esperado: el servidor arranca sin error e imprime el montaje de la documentación.
Abre en el navegador:
Y verifica la spec por consola:
Resultado esperado: el JSON del documento OpenAPI ("openapi": "3.0.3"). Detén el servidor con Ctrl+C antes de continuar.
Checklist de cierre:
- [ ]
clients.swagger.tsexiste con tags, paths y schemas deClient - [ ]
src/swagger/index.tsagrega módulos y monta la UI - [ ]
Appllama asetupSwaggermediantedocs() - [ ]
GET /api/docsmuestra Swagger UI - [ ]
GET /api/docs.jsondevuelve el documento OpenAPI - [ ]
npm run devarranca sin error
Con todos en verde, el ISS-05 está cumplido y puedes pasar al ISS-06 — Feature ProductType, donde añadirás una segunda entidad con su propio módulo Swagger.
Navegación de la ruta: ← ISS-04 · 🛠 Construir · ↑ Ruta Express · → ISS-05 · 🛠 Construir