Documentando un api con swagger

Puede que en alguna ocasión os haya sucedido que una ver terminado el desarrollo de un API os preguntéis ¿y ahora, cómo le explico al equipo la forma de usarla?, ¿les listo todos los métodos y parámetros en un mail?, ¿llamo por teléfono y explico cada endpoint y los datos que consume?. Para este tipo de situaciones disponemos de swagger, con esta herramienta vamos a conseguir crear una interfaz que por sí sola explicará nuestros métodos y nos permitirá probarlos ¿parece interesante, verdad? ¡ pues al lío! 1. Primeros pasos 2. Definición básica - Objeto OpenApi 3. Components - Objetos reusables 4. Schemas - Modelos de datos 5. Parameters 6. Paths y operaciones 7. Responses 8. RequestBody 9. Autenticación y autorización 10. Ejemplo completo 11. Desplegar la documentación

Primeros pasos

Lo primero que tenemos que hacer es abrir la herramienta Swagger Editor y su api de ejemplo. En esta demo podemos observar un ejemplo de definición de API e ir familiarizándonos con los conceptos básicos de la misma. Esta herramienta está divida en dos, por un lado, un editor, donde en formato YAML iremos codificando la descripción de nuestro API, y por otro una imagen previa, donde observar el resultado final de nuestros ajustes. La edición de nuestro archivo YAML se basará en un conjunto de especificaciones y reglas denominado OpenApi.

También podemos descargarnos el editor de swagger en nuestro propio entorno. Una forma muy sencilla de desplegarlo es descarnos y ejecutar la imagen de docker

Después de esta breve introducción ha llegado el momento de editar nuestro archivo YAML y conocer los apartados del mismo. Comencemos por la raíz de nuestro documento:

Definición básica - Objeto OpenApi

El objeto OpenApi es la raíz de nuestro documento, que a su vez contiene campos básicos. Veamos un ejemplo de alguno de estos campos:
openapi: 3.0.0
info:
  description: Especificación de API para proyecto de ejemplo.
  version: 1.0.0
  title: API proyecto de ejemplo
servers:
  - url: 'https://antoniofernandez.com'

paths:
  #aquí nuestros paths
components:
  #componentes reutilizables
security:
  #seguridad
En este bloque podemos observar varios campos:
openapi
Definirá la versión de nuestra especificación de openapi utilizada.
info
Contendrá un objeto con información de utilidad sobre nuestro api.
servers
Array con las urls de los diferentes entornos donde está alojada nuestra API, por ejemplo:
servers:
  - url: 'https://antoniofernandez.com'
    description: 'Entorno de producción de apis'
  - url: 'https://development.antoniofernandez.com'
    description: 'Entorno de desarrollo de apis'
paths
Rutas de nuestra aplicación
components
Componentes que reutilizaremos a lo largo de nuestro documento
security
Seguridad general aplicada a todos los endpoints de nuestro API

Components - Objetos reusables

En este apartado definiremos aquellos componentes que podremos reutilizar a lo largo de nuestro documento, como por ejemplo, los modelos de nuestra implementación. Podemos observar diferentes secciones.
components:

  parameters:
  responses:
  schemas:
  securitySchemes:

Schemas

En esta sección definiremos las entidades utilizadas en nuestro API, es decir los datos que mostrará o consumirá esta misma. El objetivo es definir la estructura básica de los datos con los que trabajamos (podríamos decir que los modelos de base de datos). Una vez que los hayamos definido en esta sección, podremos asignarlos a las operaciones que los consuman o devuelvan.
  schemas:    
 
    User:
      type: object
      required: 
        - email
        - password
        - role
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        username:
          type: string
        password:
          type: string
          format: password
        email:
          type: string
          format: email
        role:
          type: string
          enum: 
            - superadmin
            - editor
En este bloque estamos definiendo un objeto User , que se corresponderá con nuestro módelo User en nuestro API. A continuación, enumero las propiedades utilizadas:
required
Array con los campos que son requeridos a la hora de crear un registro de nuestro modelo
properties
Campos del objeto
Cada campo a su vez, se describirá utilizando diferentes propiedades:
type
tipo de dato (integer, string, object)
readOnly / writeOnly
Indica si es un campo que solo se muestra cuando realizamos peticiones de consulta, pero no se necesita enviar cuando insertamos o actualizamos, o al revés.

Normalmente los campos id suelen ser un candidato ideal para ser readOnly, ya que cuando creamos nuestro modelo, no necesitamos enviarlo, sino que se genera de forma automática.

format
Indica diferentes formatos para el tipo string (password, email, uuid, date)
enum
Listado de posibles valores que puede tomar la propiedad.

Más adelante podremos ver cómo definir estos modelos en línea (sin necesidad de ser globales), no obstante como la mayoría se reutilizan en varios métodos de nuestro API, la forma más óptima es crearlos dentro del objeto global components.schemas.

Parameters

Los parámetros se pueden definir a nivel de operación o path y tambíen de forma global en el objeto components.parameter. Veamos varios ejemplos de esto:
  
#definición a nivel de path
  /users:
    get:
      summary: listar usuario
      description: Este método lista usuarios
      parameters: 
        - name: name 
          in: query
          description: 'Buscar por nombre'
          schema:
            type: string
Podemos observar varios atributos importantes:
name
Nombre del parámetro, es decir , el nombre con el que lo capturaremos.
in
Indicándonos la localización del parámetro (query, path, header, cookie) , en función de si el parámetro está presente en la url, como parámetro del path, en una cabecera o en una cookie .
description
Información descriptiva
example
Podemos indicar un valor de ejemplo de este parámetro
schema.type
Tipo de dato del parámetro
También podremos definirlos en la seccion parameters de la raíz de nuestro documento, haciéndolos de esta forma global al mismo y por ende reutilizables.

Algunos ejemplos de parámetros que son el candidato ideal para ser definidos de forma global, son aquellos para realizar paginación u ordenación, ya que irán incluidos en varias de nuestras peticiones.

  parameters:
    sortParam:
      name: sort
      in: query
      description: "Realizar ordenacion"
      example: "+fecha -nombre"
      schema:
        type: string
        
        
    limitParam:
      name: limit
      in: query
      description: "número de resultados"
      example: 50
      schema:
        type: integer    
    
    
    skipParam:
      name: skip
      in: query
      description: "número del que partir"
      example: 50
      schema:
        type: integer

Al establecer nuestros parámetros como globales, podremos reutilizarlos en varias operaciones, referenciándolos mediante $ref. Esto nos ahorrará mucho trabajo, evitando que tengamos que repetir la definición de los mismos.

Paths y operaciones

En OpenApi los paths son los endpoints, como puede ser /users o /users/:userId y las operaciones, los diferentes tipos de métodos http que les aplicamos (GET, POST, PUT, DELETE, etc..). A continuación veamos un ejemplo completo de la definición de varios path y operaciones:
  '/users':
   get:
    security:
      - bearerAuth: []
    tags:
      - user
    summary: 'Lista los usuarios del sistema buscando por nombre'
    parameters:
      - $ref: "#/components/parameters/sortParam"
      - $ref: "#/components/parameters/limitParam"
      - $ref: "#/components/parameters/skipParam"
      - name: nombre
        in: query
        description: 'Nombre a buscar'
        required: false
        schema:
          type: string
    responses:
      '200':
        description: Operación correcta
        content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'        
      '422':
        description: Error de validación
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Error'
      '500':
        description: Error de servidor
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Error'            
   post:
    security:
      - bearerAuth: []
    tags: 
      - user
    summary: 'Crea un nueo usuario'  
    requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
    responses:
      '200':
        description: Operación correcta
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'  
      '422':
        description: Error de validación
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Error'
      '500':
        description: Error de servidor
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Error' 

En el anterior código podemos observar la definición de un endpoint /users y la operación get y post sobre si mismo.

Observemos cómo quedaría visualmente esta estructura:
Resultado visual de la definición de las operaciones y paths
Y una de nuestrás operaciones en detalle:
Resultado visual de la definición del listado de usuarios
A continuación, echemos un vistazo a sus atributos:
security
En este apartado referenciamos un security scheme (que posteriormente definiremos en el apartado de autorización y autenticación).
tags
Las tags sirven para agrupar nuestras operaciones. Usaremos la tag 'user' y así todas las operaciones se visualizarán agrupadas en nuestra interfaz final.
parameters
Array de parameters específicos de esta operación . De igual modo también podemos referenciar a aquellos parámetros definidos como globales haciendo uso de $ref.
responses
Es necesario que definamos una respuesta por cada operación. Las respuestas se caracterizan por su http status code , el dato retornado y su content type.
No os preocupéis si no entendéis alguna de las propiedades expuestas en el anterior ejemplo, puesto que os las explicaré en los siguientes apartados.

Responses

Las respuestas se definen a nivel de operación y nos indican el código de estado http, el dato retornado y el tipo de contenido del mismo.
      responses:
        '200':
          description: Operación correcta
        '422':
          description: Error de validación
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '404':
          description: Registro no encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'        
        '500':
          description: Error de servidor
          content:
            application/json:
              schema:
Observemos sus propiedades:
código de estado
Definido a través de un número (200, 404, 500, etc..)
description
Información descriptiva adicional.
content
Tipo de contenido, por ejemplo: application/json , application/xml, application/x-www-form-urlencoded, multipart/form-data, text/plain; charset=utf-8, text/html, etc...
schema
Aquí referenciaremos a nuestros modelos definidos anterioremente. También podremos definirlos en la propia definición de la response, pero al ser modelos que se reutlizarán en varias operaciones, lo correcto es definirlos de forma global.

RequestBody

Al igual que se definen las respuestas, también tendremos que definir la estructura de entrada de datos para aquellas operaciones que lo permitan, como las que usen PUT Y POST.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'
Podemos observar las siguientes propiedades:
required
Indicando la obligatoridad del dato.
content
Tipo de contenido, por ejemplo: application/json , application/xml, application/x-www-form-urlencoded, multipart/form-data, text/plain; charset=utf-8 ,text/html, etc...
schema
Aquí al igual que en las respuestas, referenciaremos a nuestros modelos definidos anterioremente.

Autenticación y autorización

OpenApi usa el término security schema para definir la autenticacíon y autorización. Los diferentes security schemes serán definidos en el objeto components.securitySchemes
  securitySchemes:
  
    basicAuth:
      type: http
      scheme: basic  
    
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
Podemos observar las siguientes propiedades:
type
Puede tomar diferentes valores en función del tipo de autenticación ( http, apiKey, oauth2, openIdConnect).
scheme
Dentro de cada tipo de autenticación podemos elegir entre diferentes subtipos, por ejemplo, bearer y basic dentro de la autenticación http.
bearerFormat
Propiedad arbitraria que indica el tipo de token, en este caso JWT (JSON Web Token).

Una vez que hemos definidos nuestros esquemas de seguridad, podremos aplicarlos en todas las operaciones o sólo en determinados paths y operaciones , usando para esto la propiedad security de nuestro documento raíz o de nuestra operación.

Veamos un ejemplo de como enlazar un securityScheme para todas las rutas de nuestra API:
openapi: 3.0.0
info:
  description: Especificación de API para proyecto de ejemplo.
  version: 1.0.0
  title: API proyecto de ejemplo
servers:
  - url: 'https://antoniofernandez.com'
security:
  -bearerAuth []

Ejemplo completo

Ahora que ya conocemos los componentes fundamentales en la estructura de un documento swagger, vamos a ver un ejemplo completo.
openapi: 3.0.0
info:
  description: Ejemplo para blog.
  version: 1.0.0
  title: API de ejemplo para blog
servers:
  - url: 'https://antoniofernandez.com'
    description: 'Entorno de produccíon de apis'
  - url: 'https://development.antoniofernandez.com'
    description: 'Entorno de desarrollo de apis'
paths:

  /users:
    get:
      security: 
      - bearerAuth: []
      tags: 
        - user
      summary: listar usuario
      description: Este método lista usuarios
      parameters: 
        - $ref: '#/components/parameters/sortParam'
        - $ref: '#/components/parameters/limitParam'
        - $ref: '#/components/parameters/skipParam'
        - name: name 
          in: query
          description: 'Filtro para buscar por nombre de usuario'
          schema:
            type: string
      responses:
        200:
          description: operacion correcta
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
        500:
          description: error de servidor
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
    
    post:
      security: 
      - bearerAuth: []
      tags:
        - user
      summary: crear usuarios
      description: Este método crea usuarios
      responses:
        200:
          description: operacion correcta
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/User'




components:

  parameters:
    sortParam:
      name: sort
      in: query
      description: " ordenacion"
      example: "+fecha -nombre"
      schema:
        type: string
        
        
    limitParam:
      name: limit
      in: query
      description: "número de resultados a obtener"
      example: 50
      schema:
        type: integer    
    
    
    skipParam:
      name: skip
      in: query
      description: "número de resultados desde el que partir"
      example: 0
      schema:
        type: integer    


  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      
  schemas:    
 
    User:
      type: object
      required: 
        - email
        - password
        - role
      properties:
        id:
          type: string
          format: uuid
          readOnly: true
        username:
          type: string
        password:
          type: string
          format: password
        email:
          type: string
          format: email
        role:
          type: string
          enum: 
            - superadmin
            - editor
            
            
    Error:
      type: object
      properties:
        code:
          description: Código de error
          type: string
        status:
          description: httpstatus
          type: integer
          format: int32
        type:
          type: string
          description: Tipo de error
        message:
          type: string
          description: Mensaje de error          
      
  

Desplegar la documentación

Ahora ya tenemos definido nuestro documento swagger, no obstante esto por sí solo, aún no nos sirve de nada. El siguiente paso es construir automáticamente una interfaz visual en base a él, en concreto, desplegaremos nuestra documentación sobre un API desarrollado en nodejs, bajo el framework Expressjs. Lo primero que tenemos que hacer es descargar el JSON con la definición de nuestro API del propio ditor de swagger, en el menú superior File --> "Convert and save as JSON", una vez que tenemos este fichero, lo movemos a un directorio de nuestro proyecto. Para desplegar nuestra documentación swagger necesitamos añadir un nuevo módulo a nuestro API, el módulo swagger-ui-express, utilizando para ello el siguiente comando:
npm install swagger-ui-express
y añadimos las siguientes líneas de código a nuestro app.js
const express = require('express');
const app = express();
const swaggerUi = require('swagger-ui-express');
const swaggerDocument = require('./swagger.json');

app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));
En este fragmento de código podemos ver cómo le indicamos que la ruta para consultar la documentación será /api-docs y que el fichero a buscar para comprender cómo construir la interfaz, será el swagger.json alojado en ese mismo directorio. Una vez hecho todo esto, ya podemos consultar la documentación swagger navegando a la ruta que le hemos indicado anteriormente. Espero que este artículo os haya servido de ayuda y si tenéis cualquier duda, la resolveré sin problema 😉