Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

Técnicas Potentes de OpenAPI en FastAPI: Convertir Swagger UI en una “Especificación” Viva de la API (Versionado, Diseño de Errores, Paginación y Más)

green snake

Photo by Pixabay on Pexels.com

Técnicas Potentes de OpenAPI en FastAPI: Convertir Swagger UI en una “Especificación” Viva de la API (Versionado, Diseño de Errores, Paginación y Más)


Puntos Clave al Inicio (Guía de Lectura Rápida)

  • FastAPI genera automáticamente un esquema OpenAPI a partir de tu código y te permite verlo mediante Swagger UI / ReDoc. Este artículo trata de considerar ese resultado no como “docs automáticas”, sino como una especificación que se convierte en el lenguaje compartido del equipo.
  • Las claves para mejorar la calidad de OpenAPI son la consistencia en tags, summaries, descriptions, responses, formatos de error y ejemplos, además de reglas unificadas de diseño de endpoints.
  • Vamos a traducir el “diseño que rinde en operaciones” a funcionalidades de FastAPI: versionado (/v1), reglas de compatibilidad, paginación, modelos de error compartidos y consistencia de respuestas.
  • Tras leer esto, Swagger UI mostrará claramente la forma de una “API no confusa”, y los cambios de especificación, revisiones y guía para consumidores serán mucho más fáciles.

A Quién Ayuda Esto (Audiencias Específicas)

Desarrolladores individuales y estudiantes

Si publicas una API pequeña y quieres “notas para tu yo del futuro”, esto es para ti. Un Swagger UI bien organizado te ahorra tiempo después. En particular, estandarizar los formatos de error y respuesta hace que el trabajo del front-end sea muchísimo más sencillo.

Ingenieros backend en equipos pequeños

Cuando varias personas tocan la API, el diseño suele derivar según quién implemente. Reflejar reglas básicas—tags, errores, paginación—en OpenAPI facilita mucho la alineación y las revisiones.

Equipos SaaS y startups

Cuantas más integraciones tengas (socios, otros equipos internos, apps móviles), más afecta la calidad de OpenAPI a la velocidad de desarrollo. El versionado, la compatibilidad y la claridad son costosos de incorporar a posteriori—construir la “forma” correcta desde el inicio es más seguro.


Notas de Accesibilidad (Consideraciones de Legibilidad)

  • Los puntos clave van primero, y los encabezados están redactados para que puedas entender “qué logra esta sección” de un vistazo.
  • Los términos (OpenAPI, esquema, compatibilidad, etc.) se explican brevemente en su primera aparición y luego se usan de forma consistente.
  • Los ejemplos de código son intencionalmente cortos y se dividen en bloques digeribles.
  • Cada sección puede leerse de forma independiente; el contexto necesario se incluye dentro de la sección.

En conjunto, la estructura apunta a legibilidad técnica y comprensión progresiva, con una mentalidad de accesibilidad de nivel AA para el contenido.


1. OpenAPI No Está “Bien Porque Se Genera Solo”: Es un Activo que Haces Crecer

OpenAPI es un estándar para expresar especificaciones de API en formato legible por máquina (JSON/YAML). FastAPI lo genera automáticamente y lo muestra mediante Swagger UI o ReDoc.

Pero la salida por defecto suele decir “funciona” sin transmitir claramente la intención. Por ejemplo:

  • Los endpoints están desordenados y son difíciles de encontrar
  • El significado de los parámetros no es claro
  • La forma de los errores es desconocida
  • Las respuestas exitosas no tienen ejemplos, así que quien implementa termina adivinando

Cuando eso ocurre, los consumidores (incluido tu yo futuro) acaban leyendo el código igualmente—reduciendo el valor de la especificación.

Si, en cambio, Swagger UI deja claro:

  • qué hace la API,
  • qué entradas son obligatorias,
  • qué devuelve en caso de éxito,
  • cómo son los fallos,
  • y cómo se maneja la compatibilidad,

entonces las revisiones, la implementación y la operación se vuelven más fáciles.


2. Empieza con lo Básico: Título, Descripción, Tags y Organización de Routers

2.1 Agrega metadatos de la API

Usa los parámetros de FastAPI() para dar forma a lo que aparece en la documentación.

from fastapi import FastAPI

app = FastAPI(
    title="Example API",
    description="A sample API built with FastAPI. This project treats OpenAPI as a living specification.",
    version="1.0.0",
    contact={"name": "Support", "email": "support@example.com"},
    license_info={"name": "MIT"},
)

Esto aumenta la confianza de usuarios externos y la claridad dentro del equipo.

2.2 Usa tags (Swagger UI se vuelve navegable)

Define tags consistentes por router para que los endpoints queden agrupados.

from fastapi import APIRouter

router = APIRouter(prefix="/users", tags=["users"])

Define una regla de equipo para el nombre de los tags (por ejemplo: sustantivo, minúsculas, plural): users, articles, auth, admin.

2.3 Separación de routers + base para versionado

Cubriremos el versionado más adelante, pero prepara la base desde ahora.

from fastapi import FastAPI
from app.api.v1.routers import users, articles

app = FastAPI(title="Example API", version="1.0.0")

app.include_router(users.router, prefix="/v1")
app.include_router(articles.router, prefix="/v1")

Un prefijo /v1 hace que gestionar compatibilidad sea mucho más fácil.


3. Eleva la Calidad de la Especificación: summary, description y response_model

3.1 Agrega un “summary” y “details” por endpoint

FastAPI admite documentación OpenAPI más rica mediante argumentos del decorador.

from fastapi import APIRouter
from app.schemas import UserRead

router = APIRouter(prefix="/users", tags=["users"])

@router.get(
    "",
    summary="List users",
    description="Returns a user list for admin screens. Supports search and pagination.",
    response_model=list[UserRead],
)
def list_users():
    ...

Mantén summary corto; pon los matices en description.

3.2 Usa response_model para “fijar” la forma de las respuestas

Los modelos de respuesta explícitos hacen el esquema más limpio y reducen cambios incompatibles accidentales.

from pydantic import BaseModel

class UserRead(BaseModel):
    id: int
    name: str
    email: str

Sin response_model, cualquier dict que devuelvas se vuelve el contrato de facto y puede desviarse. Con él, también puedes eliminar campos innecesarios y controlar la exposición.


4. Agrega Ejemplos: Eliminan Rápidamente la Confusión del Consumidor

En Swagger UI, los ejemplos suelen ser lo más útil para los consumidores. Un solo ejemplo de entrada/salida acelera muchísimo la comprensión.

4.1 Ejemplos del cuerpo de la solicitud

Con Pydantic v2, usa json_schema_extra.

from pydantic import BaseModel, Field

class UserCreate(BaseModel):
    name: str = Field(..., description="Display name")
    email: str = Field(..., description="Email address")

    model_config = {
        "json_schema_extra": {
            "examples": [
                {"name": "Hanako Yamada", "email": "hanako@example.com"}
            ]
        }
    }

4.2 Ejemplos de respuesta (múltiples casos)

Puedes agregar ejemplos mediante responses.

from fastapi import APIRouter, status
from app.schemas import UserRead

router = APIRouter(prefix="/users", tags=["users"])

@router.post(
    "",
    summary="Create a user",
    response_model=UserRead,
    status_code=status.HTTP_201_CREATED,
    responses={
        201: {
            "description": "Created",
            "content": {
                "application/json": {
                    "example": {"id": 1, "name": "Hanako Yamada", "email": "hanako@example.com"}
                }
            },
        }
    },
)
def create_user():
    ...

No necesitas muchos ejemplos—solo los que más suelen necesitar los consumidores: entrada, éxito y “fallo típico”.


5. Diseño de Errores: Un Formato Compartido Cambia la Usabilidad de la API

El mayor dolor de los consumidores es: “Cuando falla, ¿cómo lo manejo?” Si los errores son consistentes, el código front-end y los SDK se vuelven mucho más fáciles.

5.1 Define un modelo de error compartido

from pydantic import BaseModel
from typing import Any

class ErrorDetail(BaseModel):
    code: str
    message: str
    detail: dict[str, Any] | None = None

class ErrorResponse(BaseModel):
    error: ErrorDetail
  • code: legible por máquina (p. ej., USER_NOT_FOUND)
  • message: texto humano breve
  • detail: info estructurada opcional (p. ej., campos en errores de validación)

5.2 Documenta errores representativos en responses

from fastapi import APIRouter, HTTPException
from app.schemas import UserRead, ErrorResponse

router = APIRouter(prefix="/users", tags=["users"])

@router.get(
    "/{user_id}",
    summary="Get user details",
    response_model=UserRead,
    responses={
        404: {
            "model": ErrorResponse,
            "description": "User does not exist",
            "content": {
                "application/json": {
                    "example": {
                        "error": {
                            "code": "USER_NOT_FOUND",
                            "message": "User not found",
                            "detail": {"user_id": 999}
                        }
                    }
                }
            }
        }
    },
)
def get_user(user_id: int):
    user = None
    if not user:
        raise HTTPException(status_code=404, detail="not found")
    return user

Tal como está escrito, HTTPException.detail aún no está en tu formato compartido—por lo que en la práctica querrás handlers de excepciones.

5.3 Unifica errores mediante exception handlers

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException

from app.schemas import ErrorResponse

app = FastAPI()

@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
    code = "HTTP_ERROR"
    if exc.status_code == 404:
        code = "NOT_FOUND"
    body = ErrorResponse(error={"code": code, "message": str(exc.detail), "detail": None})
    return JSONResponse(status_code=exc.status_code, content=body.model_dump())

@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
    body = ErrorResponse(
        error={
            "code": "VALIDATION_ERROR",
            "message": "Invalid input",
            "detail": {"errors": exc.errors()},
        }
    )
    return JSONResponse(status_code=422, content=body.model_dump())

Ahora tanto los errores HTTP como los de validación comparten la misma forma. Los clientes pueden ramificar por error.code y mostrar error.message.


6. Diseño de Paginación: Convierte un Área Confusa en una “Forma Estándar”

Las APIs de listas casi siempre necesitan paginación. Si improvisas, aparecerán problemas de compatibilidad más tarde.

6.1 offset/limit (base)

from pydantic import BaseModel
from typing import Generic, TypeVar

T = TypeVar("T")

class PageMeta(BaseModel):
    limit: int
    offset: int
    total: int

class PageResponse(BaseModel, Generic[T]):
    items: list[T]
    meta: PageMeta
from fastapi import APIRouter, Query
from app.schemas import UserRead, PageResponse

router = APIRouter(prefix="/users", tags=["users"])

@router.get(
    "",
    summary="List users",
    response_model=PageResponse[UserRead],
)
def list_users(
    limit: int = Query(20, ge=1, le=100, description="Number of items (max 100)"),
    offset: int = Query(0, ge=0, description="Start offset"),
):
    items = []
    total = 0
    return {"items": items, "meta": {"limit": limit, "offset": offset, "total": total}}

Incluir meta.total es importante para paginadores y pantallas de “conteo total”.

6.2 Paginación basada en cursor (avanzada)

Para datasets grandes o listas que cambian con frecuencia, offset puede ser lento o inestable (deriva de páginas). La paginación por cursor devuelve next_cursor.

from pydantic import BaseModel

class CursorPageMeta(BaseModel):
    next_cursor: str | None = None

class CursorPageResponse(BaseModel):
    items: list[UserRead]
    meta: CursorPageMeta

Operativamente, estandarizar en un método es lo más fácil. Si los mezclas, documenta la regla (p. ej., pantallas admin usan offset; feeds usan cursor).


7. Versionado y Compatibilidad: Diseñar Cambios que Puedas “Anunciar”

7.1 Empieza con /v1

Si introduces /v1 temprano, los cambios incompatibles futuros se vuelven manejables.

  • Cambios compatibles hacia atrás (agregar campos, mejorar descripciones) se quedan en /v1
  • Cambios incompatibles (renombrar campos, cambiar formatos de respuesta) van a /v2

7.2 Reglas mínimas de compatibilidad (esto vale oro)

La mayoría de incidentes de compatibilidad vienen de “pequeños cambios casuales”. Como mínimo:

  • No elimines campos existentes (depreca → ventana de migración → elimina)
  • No cambies el significado de campos existentes
  • Ten cuidado con enums (agregar suele ser más seguro; eliminar es arriesgado)
  • No rompas el formato de error (los clientes dependen muchísimo de él)

FastAPI permite marcar endpoints como deprecados para que Swagger UI lo muestre:

@router.get(
    "/legacy",
    summary="Legacy endpoint (deprecated)",
    deprecated=True,
)
def legacy():
    ...

Eso por sí solo se convierte en una señal fuerte de “por favor migra”.


8. Controlar Cómo se Expone OpenAPI: docs_url, openapi_url y separación por entorno

En producción, quizá quieras restringir la documentación. Servicios internos pueden mantenerla abierta; servicios externos podrían bloquearla.

import os
from fastapi import FastAPI

ENV = os.getenv("ENVIRONMENT", "dev")

app = FastAPI(
    title="Example API",
    version="1.0.0",
    docs_url=None if ENV == "prod" else "/docs",
    redoc_url=None if ENV == "prod" else "/redoc",
    openapi_url=None if ENV == "prod" else "/openapi.json",
)

Si deshabilitar todo se siente arriesgado, alternativas incluyen allowlists de IP, Basic auth o acceso solo para admin. La clave es elegir una política y aplicarla consistentemente.


9. Una “Forma de Diseño de API” Compartible por el Equipo (Plantillas Prácticas)

9.1 Nombres y diseño de URL (ejemplo)

  • Recursos en plural: /users, /articles
  • Leer uno: GET /users/{id}
  • Listar: GET /users?limit=...&offset=...
  • Crear: POST /users
  • Actualizar: PUT /users/{id} (reemplazar) o PATCH /users/{id} (parcial)
  • Eliminar: DELETE /users/{id}

9.2 Consistencia de respuestas (ejemplo)

  • Listas: {"items": [...], "meta": {...}}
  • Errores: {"error": {"code": "...", "message": "...", "detail": {...}}}

9.3 Barra mínima de calidad documental (ejemplo)

  • Cada endpoint tiene summary
  • Endpoints clave también tienen description + ejemplos
  • Errores comunes (400/401/403/404/422/500) están documentados en responses
  • Endpoints deprecados están marcados con deprecated=True

Incluso solo este mínimo hace que Swagger UI se vea muchísimo mejor.


10. Referencias (Si Quieres Profundizar)


Resumen: Cómo Convertir Swagger UI en una Especificación Real

  • FastAPI genera OpenAPI automáticamente, pero si se deja solo suele quedarse en “para comprobar el comportamiento”. Si mejoras tags, descripciones, ejemplos y formatos de error, se convierte en una especificación real.
  • Las mayores ganancias operativas vienen de un formato de error compartido y una forma estándar de paginación. Esto facilita mucho a clientes y operaciones.
  • El versionado es caro de incorporar después. Empieza con /v1 y construye cambios teniendo en mente reglas de compatibilidad.
  • Haz que Swagger UI sea un lugar que la gente lea agregando summary, description y examples poco a poco. Cuanto más lo hagas, más amable será el desarrollo futuro.

Un siguiente paso natural es usar OpenAPI para generación de SDKs cliente o pruebas de contrato (contract testing). Si quieres, podemos continuar con ese tema después.


Salir de la versión móvil