Servidor de Documentación Swagger y OpenAPI en Linux
OpenAPI (anteriormente Swagger) es el estándar de facto para describir APIs REST, y herramientas como Swagger UI y ReDoc generan documentación interactiva a partir de una especificación en formato JSON o YAML. Alojar tu documentación de API en un servidor Linux dedicado—con control de versiones, autenticación y pipeline CI/CD—es fundamental para equipos de desarrollo que trabajan con APIs internas y externas. Esta guía cubre la escritura de especificaciones OpenAPI, el despliegue de Swagger UI y ReDoc con Nginx, el versionado y la automatización del pipeline de documentación.
Requisitos Previos
- Ubuntu 20.04+ o Rocky Linux 8+
- Nginx instalado
- Docker (para despliegue en contenedor)
- Node.js 18+ (para herramientas de validación)
# Instalar herramientas de validación OpenAPI
npm install -g @stoplight/spectral-cli
npm install -g swagger-cli
# Verificar instalación
spectral --version
swagger-cli --version
Estructura de la Especificación OpenAPI
# api-spec/openapi.yaml - Especificación OpenAPI 3.1 completa
openapi: 3.1.0
info:
title: Mi API
description: |
# Documentación de Mi API
Esta API proporciona acceso a los recursos de nuestra plataforma.
## Autenticación
Todas las rutas requieren token JWT en el header `Authorization: Bearer <token>`.
## Rate Limiting
Las solicitudes están limitadas a 100 por minuto por IP.
version: 1.2.0
contact:
name: Equipo de API
email: [email protected]
url: https://docs.miempresa.com
license:
name: Apache 2.0
url: https://www.apache.org/licenses/LICENSE-2.0
servers:
- url: https://api.miempresa.com/v1
description: Servidor de producción
- url: https://api-staging.miempresa.com/v1
description: Servidor de staging
- url: http://localhost:3000/v1
description: Desarrollo local
# Componentes reutilizables
components:
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
ApiKeyAuth:
type: apiKey
in: header
name: X-API-Key
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
minLength: 2
maxLength: 100
example: "Juan García"
createdAt:
type: string
format: date-time
readOnly: true
Error:
type: object
required: [error, message]
properties:
error:
type: string
example: "NOT_FOUND"
message:
type: string
example: "El recurso solicitado no existe"
details:
type: object
Paginacion:
type: object
properties:
page:
type: integer
minimum: 1
default: 1
limit:
type: integer
minimum: 1
maximum: 100
default: 20
total:
type: integer
hasNextPage:
type: boolean
responses:
NoAutorizado:
description: Token inválido o expirado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
NoEncontrado:
description: Recurso no encontrado
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# Seguridad global (aplica a todos los endpoints)
security:
- BearerAuth: []
paths:
/usuarios:
get:
summary: Listar usuarios
description: Obtiene la lista paginada de usuarios del sistema
operationId: listarUsuarios
tags: [Usuarios]
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: limit
in: query
schema:
type: integer
default: 20
maximum: 100
- name: search
in: query
description: Filtrar por nombre o email
schema:
type: string
responses:
'200':
description: Lista de usuarios
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Paginacion'
- type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/Usuario'
'401':
$ref: '#/components/responses/NoAutorizado'
post:
summary: Crear usuario
operationId: crearUsuario
tags: [Usuarios]
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [email, nombre, password]
properties:
email:
type: string
format: email
nombre:
type: string
password:
type: string
minLength: 8
examples:
ejemplo_basico:
summary: Usuario básico
value:
email: "[email protected]"
nombre: "María López"
password: "contraseña123"
responses:
'201':
description: Usuario creado exitosamente
content:
application/json:
schema:
$ref: '#/components/schemas/Usuario'
Desplegar Swagger UI con Docker
# Crear directorio para la documentación
mkdir -p /opt/api-docs/{specs,nginx}
# Copiar la especificación OpenAPI
cp api-spec/openapi.yaml /opt/api-docs/specs/
# Docker Compose para Swagger UI
cat > /opt/api-docs/docker-compose.yml << 'EOF'
version: '3.8'
services:
swagger-ui:
image: swaggerapi/swagger-ui:latest
container_name: swagger-ui
environment:
- SWAGGER_JSON=/api-docs/openapi.yaml
- BASE_URL=/docs
- SWAGGER_JSON_URL=/specs/openapi.yaml
# Múltiples URLs de specs (para varias versiones)
- URLS=[{"url":"/specs/openapi.yaml","name":"v1 - API Principal"},{"url":"/specs/openapi-v2.yaml","name":"v2 - Nuevo API"}]
volumes:
- ./specs:/api-docs
ports:
- "8080:8080"
restart: unless-stopped
redoc:
image: redocly/redoc:latest
container_name: redoc
environment:
- SPEC_URL=/specs/openapi.yaml
volumes:
- ./specs:/usr/share/nginx/html/specs
ports:
- "8081:80"
restart: unless-stopped
EOF
# Iniciar los servicios de documentación
docker compose -f /opt/api-docs/docker-compose.yml up -d
# Verificar que funcionan
curl -s http://localhost:8080 | head -5
curl -s http://localhost:8081 | head -5
Desplegar ReDoc
# Alternativa: desplegar ReDoc como archivo HTML estático
mkdir -p /var/www/api-docs/specs
cat > /var/www/api-docs/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<title>API Documentación</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<style>body { margin: 0; padding: 0; }</style>
</head>
<body>
<redoc spec-url='/specs/openapi.yaml'
expand-responses="200,201"
show-extensions="true"
scroll-y-offset="0"
hide-hostname="false"
></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
</body>
</html>
EOF
# Copiar especificaciones al servidor web
cp /opt/api-docs/specs/*.yaml /var/www/api-docs/specs/
chown -R www-data:www-data /var/www/api-docs
Nginx para Servir la Documentación
cat > /etc/nginx/sites-available/api-docs << 'EOF'
server {
listen 443 ssl http2;
server_name docs.midominio.com;
ssl_certificate /etc/letsencrypt/live/docs.midominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/docs.midominio.com/privkey.pem;
root /var/www/api-docs;
index index.html;
# Servir archivos estáticos (ReDoc HTML estático)
location / {
try_files $uri $uri/ =404;
}
# Servir las especificaciones OpenAPI
location /specs/ {
alias /var/www/api-docs/specs/;
# Headers CORS para que Swagger UI pueda cargar desde otros dominios
add_header Access-Control-Allow-Origin *;
add_header Content-Type "application/yaml";
# Caché de 1 hora para los archivos de spec
expires 1h;
add_header Cache-Control "public, max-age=3600";
}
# Swagger UI (proxy al contenedor Docker)
location /swagger/ {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Proteger acceso a la documentación interna
location /internal/ {
auth_basic "Documentación Interna";
auth_basic_user_file /etc/nginx/.htpasswd-docs;
try_files $uri $uri/ =404;
}
}
EOF
# Crear contraseña para documentación interna
apt install apache2-utils
htpasswd -c /etc/nginx/.htpasswd-docs desarrollador
ln -s /etc/nginx/sites-available/api-docs /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
Versionado de la Especificación API
# Estructura de directorios para múltiples versiones
mkdir -p /var/www/api-docs/specs/{v1,v2}
cp openapi-v1.yaml /var/www/api-docs/specs/v1/openapi.yaml
cp openapi-v2.yaml /var/www/api-docs/specs/v2/openapi.yaml
# Script para validar y publicar una nueva versión
cat > /usr/local/bin/publish-api-docs.sh << 'EOF'
#!/bin/bash
# Publicar nueva versión de la especificación API
SPEC_FILE=$1
VERSION=$2
DESTINO="/var/www/api-docs/specs/$VERSION"
if [ -z "$SPEC_FILE" ] || [ -z "$VERSION" ]; then
echo "Uso: $0 <archivo-spec> <versión>"
echo "Ejemplo: $0 openapi.yaml v2"
exit 1
fi
# Validar la especificación con Spectral
echo "Validando especificación OpenAPI..."
spectral lint "$SPEC_FILE" --ruleset .spectral.yaml
if [ $? -ne 0 ]; then
echo "ERROR: La especificación tiene errores de validación"
exit 1
fi
# Validar con swagger-cli
swagger-cli validate "$SPEC_FILE"
if [ $? -ne 0 ]; then
echo "ERROR: La especificación es inválida"
exit 1
fi
# Publicar
mkdir -p "$DESTINO"
cp "$SPEC_FILE" "$DESTINO/openapi.yaml"
chown -R www-data:www-data "$DESTINO"
echo "Especificación publicada en $DESTINO"
echo "Accesible en: https://docs.midominio.com/specs/$VERSION/openapi.yaml"
EOF
chmod +x /usr/local/bin/publish-api-docs.sh
Automatización CI/CD
# .github/workflows/docs.yml - Publicar docs automáticamente al hacer push
# Para un pipeline equivalente en un servidor Linux con scripts:
cat > /usr/local/bin/ci-publish-docs.sh << 'EOF'
#!/bin/bash
# CI/CD para documentación API - ejecutar en el servidor tras git pull
set -e
REPO_DIR="/opt/api-repo"
DOCS_DIR="/var/www/api-docs"
echo "=== Pipeline de Documentación API ==="
# 1. Actualizar el repositorio
cd "$REPO_DIR"
git pull origin main
# 2. Validar todas las especificaciones
echo "Validando especificaciones..."
for spec in api-spec/*.yaml; do
echo " Validando: $spec"
spectral lint "$spec" 2>&1
if [ $? -ne 0 ]; then
echo "ERROR en $spec"
exit 1
fi
done
# 3. Publicar especificaciones
echo "Publicando especificaciones..."
cp api-spec/*.yaml "$DOCS_DIR/specs/"
# 4. Recargar Nginx para servir la nueva versión
nginx -s reload
echo "=== Documentación publicada exitosamente ==="
EOF
chmod +x /usr/local/bin/ci-publish-docs.sh
# Configurar webhook para ejecutar al hacer git push
# (usando un servidor de webhook simple)
apt install webhook
cat > /etc/webhook/hooks.json << 'EOF'
[
{
"id": "deploy-docs",
"execute-command": "/usr/local/bin/ci-publish-docs.sh",
"command-working-directory": "/opt/api-repo",
"trigger-rule": {
"match": {
"type": "payload-hash-sha256",
"secret": "webhook-secret-seguro",
"parameter": {
"source": "header",
"name": "X-Hub-Signature-256"
}
}
}
}
]
EOF
systemctl enable --now webhook
Seguridad y Control de Acceso
# Configurar acceso restringido para documentación interna
# Método 1: Autenticación básica con Nginx
apt install apache2-utils
htpasswd -c /etc/nginx/.htpasswd-api admin
# Método 2: Restricción por IP para documentación sensible
cat >> /etc/nginx/sites-available/api-docs << 'EOF'
# Solo permitir acceso a la documentación desde la red interna
location /internal-api/ {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
try_files $uri $uri/ =404;
}
EOF
nginx -t && systemctl reload nginx
# Método 3: Ocultar información sensible en la spec de producción
# Usar bundles diferentes para interno vs público
cat > /usr/local/bin/strip-internal-docs.sh << 'EOF'
#!/bin/bash
# Eliminar endpoints internos de la spec pública
# Requiere yq o python con pyyaml
python3 << 'PYTHON'
import yaml, sys
with open('openapi-full.yaml') as f:
spec = yaml.safe_load(f)
# Eliminar paths marcados con x-internal: true
spec['paths'] = {
path: details
for path, details in spec['paths'].items()
if not details.get('x-internal', False)
}
with open('openapi-public.yaml', 'w') as f:
yaml.dump(spec, f, default_flow_style=False, allow_unicode=True)
print("Spec pública generada: openapi-public.yaml")
PYTHON
EOF
chmod +x /usr/local/bin/strip-internal-docs.sh
Solución de Problemas
Swagger UI muestra error "Failed to fetch":
# Problema de CORS al cargar la spec desde otro dominio
# Añadir header CORS en Nginx para los archivos .yaml
location /specs/ {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET, OPTIONS";
}
# Verificar que la URL de la spec es correcta en la config de Swagger UI
La spec YAML no se valida:
# Verificar sintaxis YAML
python3 -c "import yaml; yaml.safe_load(open('openapi.yaml'))"
# Validar contra el estándar OpenAPI
swagger-cli validate openapi.yaml
npx @redocly/cli lint openapi.yaml
ReDoc no muestra la documentación actualizada:
# Limpiar caché del navegador
# Verificar que el archivo fue actualizado en el servidor
ls -la /var/www/api-docs/specs/
# Ver si Nginx tiene caché configurado
grep -i expires /etc/nginx/sites-available/api-docs
# Forzar recarga sin caché en Nginx (si hay proxy_cache)
nginx -s reload
Conclusión
Una documentación de API bien mantenida es tan importante como el código de la API misma: facilita la adopción por parte de desarrolladores, reduce el soporte necesario y sirve como contrato formal entre productores y consumidores de la API. El pipeline de publicación automatizado—validar la especificación con Spectral, generar la documentación con Swagger UI o ReDoc y publicarla con Nginx—garantiza que la documentación siempre está sincronizada con el código. Versionar las especificaciones OpenAPI junto al código en el mismo repositorio es la mejor práctica para mantener la consistencia a lo largo del tiempo.


