¿Qué es FastAPI?
FastAPI es un framework web moderno de Python para construir APIs. Su idea fundamental es que tus type hints son el contrato. Declara un parámetro como int, un cuerpo como un modelo de Pydantic, o un tipo de retorno como ItemOut, y el framework valida la solicitud, serializa la respuesta y escribe la documentación; todo a partir de esa única declaración.
Está construido sobre dos librerías que deberías conocer por nombre. Starlette proporciona el toolkit ASGI: enrutamiento, middleware y el ciclo de solicitud/respuesta. Pydantic se encarga de la validación y la serialización. FastAPI es la capa que los combina, añade inyección de dependencias y genera un esquema de OpenAPI al iniciar.
El resultado es que una función pequeña y legible te proporciona un endpoint validado con documentación interactiva. La magia es real, pero es mecánica, y comprender esa mecánica es el objetivo de esta guía.
Operaciones de ruta
Una operación de ruta es una función vinculada a un método HTTP y una URL. El decorador nombra el método y la firma de la función le indica a FastAPI qué esperar.
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello, world"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
El segmento {item_id} es un parámetro de ruta, y su anotación int significa que FastAPI convierte la cadena y devuelve automáticamente un 422 si no puede hacerlo. Cualquier elemento que no esté en la ruta se trata como un parámetro de consulta, y un valor predeterminado de None lo hace opcional. No hay parseo manual ni request.args.
Agrupa operaciones relacionadas con un APIRouter, que puede llevar un prefijo y etiquetas para la documentación.
from fastapi import APIRouter
router = APIRouter(prefix="/items", tags=["items"])
@router.get("")
async def list_items():
return []
@router.post("", status_code=201)
async def create_item():
return {}
Luego incluye el router en la app: app.include_router(router). Así es como un proyecto de FastAPI se mantiene modular.
Modelos de Pydantic
Un modelo de Pydantic es una clase que hereda de BaseModel. Se encarga de parsear la entrada, forzar tipos compatibles, aplicar restricciones y lanzar un error de validación claro cuando algo falla.
# app/schemas.py
from datetime import datetime
from pydantic import BaseModel, Field
class ItemBase(BaseModel):
name: str = Field(min_length=1, max_length=120)
price: float = Field(gt=0)
tags: list[str] = []
class ItemCreate(ItemBase):
pass
class ItemOut(ItemBase):
id: int
created_at: datetime
model_config = {"from_attributes": True}
Field añade restricciones y metadatos. model_config = {"from_attributes": True} permite que Pydantic lea atributos de un objeto ORM, para que puedas devolver una fila de la base de datos directamente y que sea serializada. La separación entre ItemCreate y ItemOut es deliberada: el modelo de entrada no tiene id, y el modelo de salida puede exponer campos que el cliente no puede definir.
Usa un modelo de Pydantic como el tipo del cuerpo (body) y FastAPI parseará, validará y te entregará un objeto tipado.
@app.post("/items", response_model=ItemOut, status_code=201)
async def create_item(item: ItemCreate):
return await repo.create(item)
Si el cuerpo no es válido, FastAPI devuelve un 422 con una lista de los campos exactos que fallaron. No tuviste que escribir ni una sola línea de validación.
Cuerpos de solicitud y validación
FastAPI distingue las fuentes de datos mediante la posición y el tipo:
- Los parámetros de ruta (path parameters) provienen de la URL.
- Los parámetros de consulta (query parameters) son todo lo demás en la firma.
- Un modelo de Pydantic es el cuerpo de la solicitud (request body).
Header,Cookie,FormyFilemarcan esas entradas especiales.
from fastapi import Header, Query
@app.get("/search")
async def search(
q: str = Query(min_length=1, max_length=50),
limit: int = Query(default=20, ge=1, le=100),
user_agent: str | None = Header(default=None),
):
return {"q": q, "limit": limit, "user_agent": user_agent}
Las restricciones en los query parameters se aplican de la misma manera que en los modelos, y la documentación generada muestra los límites. Esta consistencia es la razón por la cual el código de FastAPI se lee de forma tan compacta.
Documentación automática
Dado que el esquema se genera a partir del código, la documentación nunca queda obsoleta. Inicia la aplicación y abre /docs para acceder a Swagger UI, donde puedes llamar a los endpoints directamente, o /redoc para obtener una referencia más limpia.
app = FastAPI(
title="Inventory API",
version="1.0.0",
description="Items, stock levels and orders.",
)
Puedes añadir resúmenes, descripciones y ejemplos a las operaciones de ruta y a los campos, y estos aparecerán en la documentación. Para los equipos de frontend, el mismo esquema puede generar clientes tipados, asegurando que la API y sus consumidores permanezcan sincronizados. Esta es la recompensa de tratar los tipos como el contrato.
Dependencias
Una dependencia es una función que FastAPI llama antes que tu endpoint y cuyo valor de retorno es inyectado. Depends() es el mecanismo, y es composible.
from fastapi import Depends, HTTPException
async def get_session() -> AsyncSession:
async with SessionLocal() as session:
yield session
async def get_current_user(
token: str = Depends(get_token),
session: AsyncSession = Depends(get_session),
) -> User:
user = await authenticate(session, token)
if user is None:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
@app.get("/me", response_model=UserOut)
async def read_me(user: User = Depends(get_current_user)):
return user
Las dependencias pueden ser síncronas o asíncronas, pueden usar yield para ejecutar una limpieza después de la respuesta, y se almacenan en caché dentro de una solicitud, por lo que get_session se llama una sola vez incluso si varias dependencias lo necesitan. Son el lugar idiomático para gestionar sesiones de base de datos, autenticación, paginación y feature flags.
Async frente a sync
FastAPI soporta endpoints tanto def como async def, y la elección es importante.
- Un endpoint
async defse ejecuta en el event loop. Cada llamada de I/O en su interior debe ser esperada (awaited) mediante una librería async, o de lo contrario bloqueará el loop para todas las demás solicitudes. - Un endpoint
defconvencional se ejecuta en un thread pool, por lo que las llamadas bloqueantes son seguras, pero cada solicitud ocupa un hilo.
# Async: use an async driver and await it
@app.get("/items")
async def list_items(session: AsyncSession = Depends(get_session)):
result = await session.execute(select(Item))
return result.scalars().all()
# Sync: blocking code is fine; FastAPI offloads it
@app.get("/report")
def build_report():
return generate_report() # CPU or blocking I/O
La regla es sencilla: async en todo el camino, o sync en todo el camino. Mezclar una llamada bloqueante dentro de un endpoint async def es el error de rendimiento más común en FastAPI, y es invisible hasta que llega el tráfico.
Errores y códigos de estado
Lanza HTTPException para devolver un error con un código de estado y un detalle en JSON. FastAPI lo convierte en una respuesta y documenta el código de estado.
from fastapi import HTTPException, status
@app.get("/items/{item_id}", response_model=ItemOut)
async def read_item(item_id: int):
item = await repo.get(item_id)
if item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Item not found",
)
return item
Para mantener una estructura de error consistente en toda la API, registra manejadores de excepciones que conviertan los errores del framework y del dominio en un único envoltorio. Mantén el campo detail estable; los clientes lo utilizarán para el parseo.
Tareas en segundo plano
BackgroundTasks ejecuta trabajo después de que se envía la respuesta, dentro del mismo proceso. Es la opción ligera para enviar un correo electrónico o escribir una fila de auditoría.
from fastapi import BackgroundTasks
def send_receipt(email: str) -> None:
...
@app.post("/orders", status_code=201)
async def create_order(
order: OrderCreate,
tasks: BackgroundTasks,
):
saved = await repo.create(order)
tasks.add_task(send_receipt, saved.email)
return saved
Las tareas en segundo plano no son una cola de trabajos. Se ejecutan en el proceso, por lo que se pierden si el worker se reinicia y compiten con el manejo de solicitudes. Para cualquier cosa que requiera durabilidad, delega la tarea a Celery, RQ o a una cola gestionada.
Bases de datos
FastAPI no tiene una capa de base de datos, por lo que debes elegir la tuya. Las opciones más comunes son SQLAlchemy con un motor async o SQLModel, que combina SQLAlchemy con Pydantic. El patrón de dependencias se encarga de suministrar la sesión y cerrarla después de la solicitud.
# app/db.py
from collections.abc import AsyncIterator
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
engine = create_async_engine(settings.database_url, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False)
async def get_session() -> AsyncIterator[AsyncSession]:
async with SessionLocal() as session:
yield session
@router.post("", response_model=ItemOut, status_code=201)
async def create_item(
payload: ItemCreate,
session: AsyncSession = Depends(get_session),
):
item = Item(**payload.model_dump())
session.add(item)
await session.commit()
await session.refresh(item)
return item
Usa expire_on_commit=False para que los objetos sigan siendo utilizables después del commit, y mantén las transacciones dentro de la solicitud. Si prefieres un driver síncrono, utiliza endpoints def estándar para que FastAPI pueda delegarlos al thread pool.
Seguridad
FastAPI incluye los componentes básicos para OAuth2 y JWT sin imponer un sistema de autenticación completo. OAuth2PasswordBearer extrae el token, y python-jose o pyjwt lo verifican.
from fastapi.security import OAuth2PasswordBearer
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/users/me", response_model=UserOut)
async def read_me(
token: str = Depends(oauth2_scheme),
session: AsyncSession = Depends(get_session),
):
user = await get_user_from_token(session, token)
if user is None:
raise HTTPException(status_code=401, detail="Invalid token")
return user
Almacena los hashes de las contraseñas con passlib o argon2-cffi, nunca en texto plano. Realiza el hash y la verificación fuera del endpoint, mantén los secretos en la configuración y añade rate limiting y CORS de manera deliberada. FastAPI te proporciona las herramientas; la política sigue siendo responsabilidad tuya.
Configuración y ajustes
Mantén la configuración en un único objeto tipado en lugar de leer variables de entorno en todo el código. pydantic-settings valida el entorno al iniciar la aplicación y te ofrece la misma seguridad de tipos que tus modelos.
# app/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="APP_")
database_url: str
secret_key: str
access_token_minutes: int = 30
settings = Settings()
from functools import lru_cache
from app.config import Settings
@lru_cache
def get_settings() -> Settings:
return Settings()
@app.get("/info")
async def info(settings: Settings = Depends(get_settings)):
return {"token_minutes": settings.access_token_minutes}
Dado que los ajustes se inyectan como cualquier otra dependencia, las pruebas pueden sobrescribirlos con app.dependency_overrides[get_settings]. Si faltan variables obligatorias, la aplicación fallará al iniciar en lugar de hacerlo en la primera solicitud, que es precisamente cuando quieres detectarlo.
Pruebas
TestClient envuelve la aplicación en un cliente similar a WSGI y realiza solicitudes reales en el mismo proceso, incluyendo la validación y la anulación de dependencias (dependency overrides).
# tests/test_items.py
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_create_item():
response = client.post("/items", json={"name": "Cup", "price": 9.5})
assert response.status_code == 201
assert response.json()["name"] == "Cup"
def test_invalid_price():
response = client.post("/items", json={"name": "Cup", "price": -1})
assert response.status_code == 422
Usa app.dependency_overrides para sustituir la sesión de la base de datos por una de prueba, y escribe pruebas asíncronas con httpx.AsyncClient cuando necesites ejecutar endpoints asíncronos directamente. Debido a que las dependencias son explícitas, reemplazarlas en las pruebas es muy sencillo.
Mejores prácticas
- Declara los modelos de request y response explícitamente; nunca aceptes ni devuelvas diccionarios simples.
- Mantén los endpoints de
async deftotalmente async, o utilizadefpara que FastAPI delegue el trabajo bloqueante. - Coloca la lógica compartida en dependencias de
Depends()en lugar de repetirla en cada ruta. - Divide la aplicación con
APIRoutery asigna a cada router un prefijo y etiquetas. - Añade restricciones con
FieldyQueryen lugar de validar manualmente. - Lanza
HTTPExceptioncon el código de estado correcto y undetailestable. - Sobrescribe las dependencias en los tests en lugar de hacer patching de los componentes internos.
- Guarda los secretos en objetos de configuración leídos desde el entorno.
Errores comunes
- Ejecutar código bloqueante dentro de un endpoint
async defy congelar el event loop. - Retornar objetos ORM sin un
response_model, filtrando columnas internas. - Olvidar
from_attributes = Truey preguntarse por qué falla la serialización. - Escribir un único
main.pygigante en lugar de utilizar routers y módulos. - Tratar
BackgroundTaskscomo una cola de trabajos persistente. - Compartir una única sesión entre peticiones o entre tareas asíncronas.
- Validar la misma entrada dos veces porque se ignoró el modelo de Pydantic.
- Capturar todas las excepciones y retornar un 200 con un cuerpo de error.
Próximos pasos
FastAPI convierte los type hints en una API validada y documentada, razón por la cual se ha convertido en la opción predeterminada para los nuevos servicios en Python. Para comparar las ventajas y desventajas, lee la guía de Flask para conocer el enfoque síncrono minimalista y la de Django para un framework completo con ORM y panel de administración. Después, profundiza en el contrato de la API con OpenAPI y las reglas de diseño en REST APIs, y asegúrate de que tus endpoints valgan la pena documentar definiendo correctamente el modelo de recursos.