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.
Notasi la documentación debe funcionar sin acceso a Internet (una red interna aislada), descarga los archivos
swagger-ui.css,swagger-ui-bundle.jsyswagger-ui-standalone-preset.jsdesde jsDelivr a/var/www/api-docs/swagger/y cambia las rutas a relativas.
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.typesde Nginx no incluye YAML, así que sin el bloquetypeslos archivos se servirían comoapplication/octet-streamy 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 respuestas304 Not Modifiedcuando 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.
