OpenAPI es el formato estándar para describir APIs REST en un archivo YAML o JSON, y a partir de ese archivo herramientas como Swagger UI y ReDoc generan documentación interactiva. En esta guía publicarás la documentación de una API en un servidor Ubuntu 24.04 con Nginx: validarás la especificación con Spectral, servirás Swagger UI y ReDoc como páginas estáticas, organizarás varias versiones de la API, activarás HTTPS con Let's Encrypt y, si la documentación es interna, la protegerás con contraseña.

Requisitos previos

  • Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath.
  • Un usuario no root con privilegios sudo.
  • Un dominio con un registro A que apunte a la IP del servidor. En esta guía se usa docs.your_domain; sustitúyelo por el tuyo.
  • Los puertos 80 y 443 accesibles desde Internet.

No hace falta Docker ni Node.js: Swagger UI y ReDoc son aplicaciones JavaScript que se ejecutan en el navegador, así que el servidor solo sirve archivos estáticos.

Paso 1: Instalar Nginx y preparar el directorio

Instala Nginx y permite el tráfico HTTP y HTTPS en el cortafuegos:

sudo apt update
sudo apt install nginx
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable

Crea la estructura de directorios. Cada versión mayor de la API tendrá su propio subdirectorio en specs/, y Swagger UI irá en /swagger/:

sudo mkdir -p /var/www/api-docs/{specs/v1,swagger}
sudo chown -R "$USER":www-data /var/www/api-docs

Comprueba que Nginx está activo:

systemctl status nginx --no-pager
● nginx.service - A high performance web server and a reverse proxy server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: enabled)
     Active: active (running) since ...

Paso 2: Escribir la especificación OpenAPI

Trabaja con la especificación en tu directorio personal (en un proyecto real vivirá en el mismo repositorio que el código de la API) y publícala en /var/www solo cuando pase la validación.

mkdir -p ~/api-spec
nano ~/api-spec/openapi.yaml

Este ejemplo describe una API de usuarios con autenticación JWT, esquemas reutilizables y respuestas de error comunes:

openapi: 3.1.0
info:
  title: API de ejemplo
  version: 1.2.0
  description: |
    API de gestión de usuarios.

    Todas las rutas requieren un token JWT en la cabecera `Authorization: Bearer <token>`.
  contact:
    name: Equipo de API
    email: api@your_domain
  license:
    name: Apache 2.0
    identifier: Apache-2.0
servers:
  - url: https://api.your_domain/v1
    description: Producción
tags:
  - name: Usuarios
    description: Alta y consulta de usuarios
security:
  - BearerAuth: []
paths:
  /usuarios:
    get:
      tags: [Usuarios]
      summary: Listar usuarios
      description: Devuelve la lista paginada de usuarios.
      operationId: listarUsuarios
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: Lista de usuarios
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: '#/components/schemas/Usuario'
                  total:
                    type: integer
        '401':
          $ref: '#/components/responses/NoAutorizado'
    post:
      tags: [Usuarios]
      summary: Crear usuario
      description: Da de alta un usuario nuevo.
      operationId: crearUsuario
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/NuevoUsuario'
      responses:
        '201':
          description: Usuario creado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Usuario'
        '401':
          $ref: '#/components/responses/NoAutorizado'
  /usuarios/{id}:
    get:
      tags: [Usuarios]
      summary: Obtener un usuario
      description: Devuelve un usuario por su identificador.
      operationId: obtenerUsuario
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: Usuario encontrado
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Usuario'
        '401':
          $ref: '#/components/responses/NoAutorizado'
        '404':
          $ref: '#/components/responses/NoEncontrado'
components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  schemas:
    Usuario:
      type: object
      required: [id, email, nombre]
      properties:
        id:
          type: string
          format: uuid
          example: 550e8400-e29b-41d4-a716-446655440000
        email:
          type: string
          format: email
          example: [email protected]
        nombre:
          type: string
          example: Juan García
        createdAt:
          type: string
          format: date-time
          readOnly: true
    NuevoUsuario:
      type: object
      required: [email, nombre]
      properties:
        email:
          type: string
          format: email
        nombre:
          type: string
          minLength: 2
          maxLength: 100
    Error:
      type: object
      required: [detail]
      properties:
        detail:
          type: string
          example: El recurso solicitado no existe
  responses:
    NoAutorizado:
      description: Token ausente, inválido o caducado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
    NoEncontrado:
      description: Recurso no encontrado
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'

Si tu API está hecha con un framework que genera la especificación (FastAPI, NestJS, Spring con springdoc), exporta ese archivo en lugar de escribirlo a mano y sigue el resto de la guía igual.

Paso 3: Validar la especificación con Spectral

Un error en la especificación hace que Swagger UI muestre una página en blanco o incompleta, así que conviene validarla antes de publicarla. Spectral es un linter de OpenAPI que se distribuye como binario autónomo, sin dependencias de Node.js. Descarga la última versión para tu arquitectura (usa spectral-linux-arm64 en servidores ARM):

curl -fsSLo spectral https://github.com/stoplightio/spectral/releases/latest/download/spectral-linux-x64
sudo install -m 755 spectral /usr/local/bin/spectral
rm spectral
spectral --version

Spectral necesita un conjunto de reglas. Crea uno global que extienda las reglas oficiales de OpenAPI:

sudo mkdir -p /etc/spectral
echo 'extends: ["spectral:oas"]' | sudo tee /etc/spectral/ruleset.yaml

Valida la especificación:

spectral lint --ruleset /etc/spectral/ruleset.yaml ~/api-spec/openapi.yaml

Si todo está bien, Spectral indica que no hay resultados y termina con código 0. Si hay problemas, muestra cada uno con su línea y severidad:

/home/your_user/api-spec/openapi.yaml
 45:13  error  invalid-ref  '#/components/schemas/Usuarios' does not exist  paths./usuarios.get.responses[200]...

Los resultados de tipo warning (una operación sin descripción, por ejemplo) no impiden que la documentación funcione; los de tipo error sí deben corregirse. Con --fail-severity error el comando solo falla por errores, que es lo que usarás en el script de publicación.

Paso 4: Crear la página de Swagger UI

Swagger UI muestra cada operación con un formulario "Try it out" para lanzar peticiones reales desde el navegador. Se carga desde el paquete swagger-ui-dist de jsDelivr, fijado a la versión mayor 5 para no recibir cambios incompatibles:

nano /var/www/api-docs/swagger/index.html
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>API de ejemplo - Swagger UI</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui.css">
</head>
<body>
  <div id="swagger-ui"></div>
  <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
  <script>
    window.ui = SwaggerUIBundle({
      urls: [
        { url: "/specs/v1/openapi.yaml", name: "v1" }
      ],
      "urls.primaryName": "v1",
      dom_id: "#swagger-ui",
      deepLinking: true,
      presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
      layout: "StandaloneLayout"
    });
  </script>
</body>
</html>

La opción urls con StandaloneLayout añade un selector en la barra superior para cambiar entre versiones de la API. Cuando publiques la v2, solo tendrás que añadir otra entrada a la lista.

Paso 5: Crear la página de ReDoc

ReDoc genera una documentación de lectura en tres columnas, más cómoda para referencia pública, aunque sin formulario para probar peticiones. Será la página principal del sitio:

nano /var/www/api-docs/index.html
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>API de ejemplo - Referencia</title>
  <style>body { margin: 0; }</style>
</head>
<body>
  <redoc spec-url="/specs/v1/openapi.yaml" expand-responses="200,201"></redoc>
  <script src="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"></script>
</body>
</html>

Paso 6: Configurar Nginx

Crea el bloque de servidor para el dominio de la documentación:

sudo nano /etc/nginx/sites-available/api-docs
server {
    listen 80;
    listen [::]:80;
    server_name docs.your_domain;

    root /var/www/api-docs;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /specs/ {
        types {
            application/yaml yaml yml;
            application/json json;
        }
        add_header Cache-Control "no-cache";
        add_header Access-Control-Allow-Origin "*";
        try_files $uri =404;
    }
}

La configuración de /specs/ hace tres cosas:

  • Tipo MIME correcto. El mime.types de Nginx no incluye YAML, así que sin el bloque types los archivos se servirían como application/octet-stream y algunos navegadores los descargarían en lugar de pasarlos a Swagger UI.
  • Cache-Control: no-cache. El navegador revalida la especificación en cada visita, de modo que una versión recién publicada aparece al momento, sin perder las respuestas 304 Not Modified cuando no ha cambiado.
  • CORS abierto. Permite que otras herramientas (Postman, un Swagger UI alojado en otro dominio, generadores de clientes) descarguen la especificación. Si es privada, elimina esa línea.

Activa el sitio, comprueba la sintaxis y recarga Nginx:

sudo ln -s /etc/nginx/sites-available/api-docs /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Paso 7: Publicar versiones con un script

Copiar la especificación a mano es fácil de olvidar o de hacer sin validar. Este script valida el archivo con Spectral y solo si no hay errores lo instala en el directorio de la versión, primero en un archivo temporal y después con mv para que nadie descargue un archivo a medio copiar:

sudo nano /usr/local/bin/publicar-spec
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -ne 2 ]]; then
    echo "Uso: $(basename "$0") <archivo.yaml> <version>   (ejemplo: openapi.yaml v2)" >&2
    exit 1
fi

spec="$1"
version="$2"
destino="/var/www/api-docs/specs/${version}"

if [[ ! "$version" =~ ^v[0-9]+$ ]]; then
    echo "La versión debe tener el formato v1, v2, v3..." >&2
    exit 1
fi

spectral lint --ruleset /etc/spectral/ruleset.yaml --fail-severity error "$spec"

install -d -m 755 -o "${SUDO_USER:-root}" -g www-data "$destino"
install -m 644 "$spec" "${destino}/openapi.yaml.tmp"
mv "${destino}/openapi.yaml.tmp" "${destino}/openapi.yaml"

echo "Publicada en https://docs.your_domain/specs/${version}/openapi.yaml"

Hazlo ejecutable y publica la especificación como v1:

sudo chmod 755 /usr/local/bin/publicar-spec
sudo publicar-spec ~/api-spec/openapi.yaml v1

Comprueba que Nginx la sirve con el tipo correcto:

curl -sI http://docs.your_domain/specs/v1/openapi.yaml
HTTP/1.1 200 OK
Server: nginx/1.24.0 (Ubuntu)
Content-Type: application/yaml
Cache-Control: no-cache
Access-Control-Allow-Origin: *
...

Abre http://docs.your_domain/ para ver ReDoc y http://docs.your_domain/swagger/ para Swagger UI. Cuando saques una versión mayor nueva, publícala con sudo publicar-spec openapi.yaml v2 y añade { url: "/specs/v2/openapi.yaml", name: "v2" } a la lista urls de Swagger UI; la v1 sigue disponible para los clientes que aún la usan.

Si la especificación vive en un repositorio Git, lo natural es ejecutar este mismo spectral lint en el pipeline de CI en cada pull request, y publicar solo desde la rama principal.

Paso 8: Activar HTTPS con Let's Encrypt

Swagger UI envía tokens reales cuando se usa "Try it out", así que la documentación debe servirse por HTTPS. Instala Certbot con su plugin de Nginx:

sudo apt install certbot python3-certbot-nginx

Solicita el certificado. Certbot modifica el bloque de servidor para servir HTTPS y redirigir HTTP:

sudo certbot --nginx -d docs.your_domain

El paquete instala un temporizador de systemd que renueva el certificado automáticamente. Comprueba que la renovación funcionará:

sudo certbot renew --dry-run

Si tus servidores de API (servers en la especificación) están en otro dominio, esa API debe responder a las peticiones preflight de CORS con Access-Control-Allow-Origin: https://docs.your_domain para que "Try it out" funcione desde el navegador.

Paso 9 (opcional): Proteger la documentación interna

Si la documentación no debe ser pública, añade autenticación básica. Instala htpasswd y crea el primer usuario (te pedirá la contraseña):

sudo apt install apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd-docs your_user

Para añadir más usuarios, repite el comando sin -c, que sobrescribiría el archivo. Después abre la configuración del sitio:

sudo nano /etc/nginx/sites-available/api-docs

Añade estas dos líneas dentro del bloque server que escucha en el puerto 443 (el que ha creado Certbot), justo debajo de server_name:

    auth_basic "Documentación interna";
    auth_basic_user_file /etc/nginx/.htpasswd-docs;

Aplica los cambios y comprueba que sin credenciales se devuelve un 401:

sudo nginx -t && sudo systemctl reload nginx
curl -sI https://docs.your_domain/ | head -1
HTTP/1.1 401 Unauthorized

Como alternativa, si solo tu red de oficina o tu VPN deben acceder, sustituye la autenticación por reglas allow 203.0.113.0/24; y deny all; con tus rangos reales.

Solución de problemas

Swagger UI muestra "Failed to load API definition". Abre la URL de la especificación directamente en el navegador: si devuelve 404, revisa la ruta en urls; si la descarga en lugar de mostrarla, falta el bloque types en location /specs/. La consola del navegador indica si el problema es de CORS.

Swagger UI o ReDoc muestran una página en blanco. La especificación tiene un error de sintaxis YAML o una referencia rota. Ejecuta spectral lint sobre el archivo publicado para localizarlo.

Los cambios no aparecen. Comprueba que has publicado en el directorio de la versión correcta con ls -l /var/www/api-docs/specs/*/ y fuerza la recarga del navegador. Si usas una CDN delante del dominio, purga su caché para /specs/.

certbot falla con un error de validación. El registro A de docs.your_domain no apunta todavía al servidor o el puerto 80 está cerrado. Comprueba con dig +short docs.your_domain y sudo ufw status.

Conclusión

Tienes un sitio de documentación de API servido por Nginx con HTTPS, con ReDoc como referencia de lectura, Swagger UI para probar peticiones, varias versiones de la especificación y un script que impide publicar un archivo inválido. Como siguientes pasos, puedes ejecutar la validación de Spectral en el pipeline de CI de tu API, añadir reglas propias al ruleset (por ejemplo, exigir operationId en camelCase) y generar clientes SDK a partir de la misma especificación con OpenAPI Generator.