Saltar a contenido

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

⛶ Ver presentación completa ⬇ Archivo editable (.pptx)

8 diapositivas · se visualiza dentro del sitio (archivo editable .pptx como opción secundaria).


🎬 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 el bearerSecurity que 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:

EXPLICACIÓN
    ↓
CÓDIGO
    ↓
VISUAL

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

  1. Ficha del ISS
  2. Mapa mental del ISS
  3. Mapa del backend
  4. Árbol de archivos
  5. Anatomía del código
  6. Flujos
  7. Comandos explicados
  8. Recorrido del ISS, paso a paso
  9. Diagnóstico
  10. Conexión con el resto del curso
  11. Criterios de aceptación
  12. Evaluación
  13. Glosario
  14. 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: clientsSwagger exporta tags, paths y components.schemas de Client.
  • Un registry externo (src/swagger/index.ts) que fusiona los módulos de features en un documento único con buildOpenApiDocument().
  • El montaje de la UI y del JSON con setupSwagger(app).
  • El enganche en la App: src/config/index.ts importa setupSwagger y lo invoca desde el método docs().
  • 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/docs con security no 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, forbiddenResponse y unauthorizedResponse desde shared/http/swagger-security; son los fragmentos reutilizables que declaran el security y las respuestas 401/403.
  • tags — un tag Clientes que 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 lleva tags, summary, description, security y responses.
  • Parámetro id — se declara como entero minimum: 1 y con sus respuestas 400 (id inválido) y 404.
  • components.schemas — cuatro esquemas: Client, ClientCreate, ClientUpdate y ClientPatch, con ejemplos y required donde corresponde (la contraseña nunca forma parte de Client).

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: bearerSecurity y respuestas 401/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, paths y, opcionalmente, components.schemas.
  • featureSwaggerModules — arreglo con los módulos importados (clientsSwagger y, comentados, los futuros productsSwagger, userSwagger). Añadir una entidad es añadir una línea aquí.
  • buildOpenApiDocument() — recorre los módulos acumulando tags, paths y schemas, y devuelve el documento con openapi: "3.0.3", info (StoreLab API, 1.0.0), servers (usando process.env.PORT o 4000) y components.
  • setupSwagger(app) — construye el documento, monta swaggerUi.serve + swaggerUi.setup(document) en /api/docs y expone GET /api/docs.json, además de loguear las dos rutas.

Se conecta con

  • Entrada: lo llama App.docs() en src/config/index.ts.
  • Salida: importa clientsSwagger; usa swagger-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 de this.routes();, de modo que la documentación se monte al arrancar.
  • Método docs() — método privado que simplemente delega en setupSwagger(this.app), situado debajo de routes() y encima de dbConnection().

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, FK RESTRICT, í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.json
Depende 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.ts con tags, paths y schemas de Client (leyenda SIN AUTH)
  • [ ] 10.2 Existe src/swagger/index.ts que agrega módulos de features y monta UI
  • [ ] App llama setupSwagger (método docs())
  • [ ] GET /api/docs muestra Swagger UI
  • [ ] GET /api/docs.json devuelve 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 clientsSwagger con tags, 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/docs y /api/docs.json
  • [ ] config invoca setupSwagger (método docs())
mkdir -p src/swagger

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.

  1. Debajo de import { Routes } from "../routes/index"; (o debajo de los imports de BD/modelo), añadir:
import { setupSwagger } from "../swagger/index";
  1. Dentro del constructor, debajo de this.routes();, añadir:
    this.docs();
  1. Dentro de la clase App, debajo de el método routes() y encima de dbConnection(), añadir:
  private docs(): void {
    setupSwagger(this.app);
  }

Verificación ISS-05

curl -s http://localhost:4000/api/docs.json | head

Con el servidor del cierre: abrir http://localhost:4000/api/docs.

Al agregar otra entidad (patrón):

  1. Archivo nuevo features/.../<plural>.swagger.ts con : > + cat >>.
  2. PARCHE src/swagger/index.ts: debajo de import { clientsSwagger } ..., añadir el import; dentro de featureSwaggerModules, debajo de clientsSwagger,, añadir el módulo nuevo.

Cierre del ISS

npm run dev

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.ts a featureSwaggerModules.
  • 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.ts con tags, paths y schemas de Client (leyenda SIN AUTH)
  • [ ] 10.2 Existe src/swagger/index.ts que agrega módulos de features y monta UI
  • [ ] App llama setupSwagger (método docs())
  • [ ] GET /api/docs muestra Swagger UI
  • [ ] GET /api/docs.json devuelve el documento OpenAPI

Criterios internos de cada bloque (también del ISS):

  • [ ] Exporta clientsSwagger con tags, paths, components.schemas
  • [ ] Endpoints documentados como SIN AUTH
  • [ ] buildOpenApiDocument() fusiona módulos de features
  • [ ] setupSwagger(app) monta /api/docs y /api/docs.json
  • [ ] config invoca setupSwagger (método docs())

Evaluación

Preguntas de comprensión

  1. ¿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.ts lo consume y decide cómo fusionarlo y montarlo. Es el mismo principio que separa el seeder del SeedersRunner.

  2. ¿Qué devuelve buildOpenApiDocument() y para qué sirve? Devuelve un objeto OpenAPI 3.0.3 con info, servers, tags, paths y components.schemas, fusionando todos los módulos de features. Sirve tanto para la UI (swaggerUi.setup) como para el JSON de /api/docs.json.

  3. ¿Para qué existen dos rutas, /api/docs y /api/docs.json? /api/docs sirve la interfaz visual (Swagger UI) para humanos; /api/docs.json sirve el documento crudo para herramientas (clientes, pruebas, validadores). La misma spec, dos formatos.

  4. ¿Qué papel juega config y por qué no basta con crear el registry? Porque el registry es código que nadie ejecuta hasta que alguien lo llame. config engancha setupSwagger al arranque mediante this.docs(); sin ese enganche el archivo existe pero la documentación no se monta.

  5. El módulo importa bearerSecurity, forbiddenResponse y unauthorizedResponse. ¿Qué aporta eso al documento? Son fragmentos reutilizables de shared/http/swagger-security que declaran, por operación, el esquema de seguridad (security) y las respuestas normalizadas 401 y 403. Evitan repetir esos bloques en cada endpoint.

  6. ¿Cómo añadirías la documentación de una entidad nueva sin tocar el feature existente? Creando features/.../<plural>.swagger.ts que exporte su módulo, e importándolo/añadiéndolo en featureSwaggerModules de src/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:

npm run dev

Resultado esperado: el servidor arranca sin error e imprime el montaje de la documentación.

Abre en el navegador:

http://localhost:4000/api/docs

Y verifica la spec por consola:

curl -s http://localhost:4000/api/docs.json | head

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.ts existe con tags, paths y schemas de Client
  • [ ] src/swagger/index.ts agrega módulos y monta la UI
  • [ ] App llama a setupSwagger mediante docs()
  • [ ] GET /api/docs muestra Swagger UI
  • [ ] GET /api/docs.json devuelve el documento OpenAPI
  • [ ] npm run dev arranca 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