Referencia: Un Backend SaaS con FastAPI
Un diseño de referencia para una API SaaS B2B multi-inquilino: FastAPI 0.115+, Pydantic v2, Postgres, Redis, workers Celery y observabilidad integradas desde el primer día, desplegable en Kubernetes con flags y migraciones expandir-contraer.
Receta
Tarjeta de receta de referencia rápida, lista para copiar y pegar.
acme-api/
src/acme_api/
main.py # Fábrica de aplicaciones FastAPI
api/routes/ # Routers HTTP
domain/ # lógica de negocio (sin framework)
infra/db.py # Sesión de SQLAlchemy
workers/tasks.py # Tareas de Celery
alembic/ # migraciones
tests/
Dockerfile # Python 3.14 slim + uv syncCuándo usar esto:
- Backend SaaS desde cero (Greenfield)
- Enseñar el estándar de la flota a un nuevo equipo
- Comparar tu diseño con una línea base
- Apéndice RFC para ADR de Greenfield
Ejemplo de Trabajo
"""src/acme_api/main.py - fábrica mínima de aplicaciones SaaS."""
from __future__ import annotations
from contextlib import asynccontextmanager
from fastapi import FastAPI
from pydantic import BaseModel
from acme_api.api.routes import tenants, webhooks
from acme_api.infra.db import init_db
class Health(BaseModel):
status: str
@asynccontextmanager
async def lifespan(app: FastAPI):
init_db()
yield
def create_app() -> FastAPI:
app = FastAPI(title="Acme API", version="1.0.0", lifespan=lifespan)
app.include_router(tenants.router, prefix="/v1/tenants")
app.include_router(webhooks.router, prefix="/v1/webhooks")
@app.get("/health/ready", response_model=Health)
def ready() -> Health:
return Health(status="ok")
return app
app = create_app()"""src/acme_api/domain/billing.py - lógica sin framework."""
from __future__ import annotations
from decimal import Decimal
def compute_total(subtotal: Decimal, tax_rate: Decimal) -> Decimal:
return (subtotal * (Decimal("1") + tax_rate)).quantize(Decimal("0.01"))"""src/acme_api/workers/tasks.py"""
import os
from celery import Celery
celery_app = Celery("acme", broker=os.getenv("REDIS_URL", "redis://localhost:6379/0"))
@celery_app.task(bind=True, max_retries=5)
def send_invoice_email(self, invoice_id: str) -> None:
...Diagrama de arquitectura (lógico):
Cliente → Ingress → FastAPI (workers Gunicorn/Uvicorn)
↓ SQLAlchemy
Postgres
↓ encolar
Redis → workers Celery → S3 / emailLo que esto demuestra:
- El paquete
domainno tiene importaciones de FastAPI - núcleo testeable - Routers delgados; modelos Pydantic en los límites
- Celery maneja efectos secundarios asíncronos
- Endpoint de salud para la preparación de Kubernetes
Análisis Profundo
Cómo Funciona
- Multi-tenancy -
tenant_iden filas o esquema por inquilino; middleware establece la variable de contexto. - Autenticación - JWT o sesión mediante inyección de dependencias; alcances por ruta.
- Migraciones - Alembic expandir-contraer; ejecutadas en el trabajo de lanzamiento antes del tráfico.
- Observabilidad - JSON de structlog + instrumentación de FastAPI con OpenTelemetry.
- Despliegue - Imagen única para la API; la imagen del worker comparte el lockfile; etiqueta
git sha.
Opciones de Pila Tecnológica
| Capa | Elección | Por qué |
|---|---|---|
| API | FastAPI 0.115+ | OpenAPI, nativo de Pydantic |
| DB | Postgres 16 + SQLAlchemy 2 | Maduro, opción asíncrona más adelante |
| Cola | Celery + Redis | Patrones de reintentos, DLQ |
| Configuración | pydantic-settings | Arranque validado |
| Herramientas | uv 0.6+ | CI rápido, dependencias bloqueadas |
Notas de Python
FROM python:3.14.0-slim
COPY uv.lock pyproject.toml ./
RUN pip install uv && uv sync --frozen --no-dev
COPY src/ ./src/
CMD ["gunicorn", "acme_api.main:app", "-k", "uvicorn.workers.UvicornWorker"]Errores Comunes
- Controladores de ruta gordos - Monolito in-testable en archivos. Solución: servicios de dominio + routers delgados.
- Modelos ORM compartidos en workers sin pruebas de contrato - Deriva de esquema. Solución: cargas útiles de trabajos de Pydantic versionadas.
- Guardia de inquilino faltante en consultas - Fuga de datos. Solución: la capa de repositorio requiere
tenant_id. - Base de datos síncrona en rutas asíncronas sin plan - Bloqueo de bucle más tarde. Solución: documentar la elección síncrona; pool de hilos para CPU.
- Falta de idempotencia en webhooks - Efectos secundarios duplicados. Solución: almacén
Idempotency-Key.
Alternativas
| Alternativa | Usar Cuando | No Usar Cuando |
|---|---|---|
| Django + DRF | CRM con mucho backend administrativo | Microservicio solo API |
| Dividir microservicios temprano | Escala independiente probada | Equipo <8 personas |
| Serverless Lambda | Tráfico bajo y esporádico | Trabajos largos tipo Celery |
| Flask | Mínimo legado | Greenfield con enfoque OpenAPI primero |
Preguntas Frecuentes
¿Cuándo dividir los workers en un repositorio separado?
Cuando la cadencia de lanzamiento o el dominio de fallo divergen; hasta entonces, monorepo con paquetes.
¿SQLAlchemy asíncrono desde el primer día?
Opcional; síncrono + descarga clara de hilos está bien a RPS moderados.
¿Cómo manejar multi-región?
Réplicas de lectura, flags de residencia de datos del inquilino, replicar almacenamiento de objetos.
¿Integración de facturación?
Webhooks de Stripe con tabla de idempotencia; Celery para cumplimiento.
¿Estrategia de pruebas?
Pruebas unitarias pytest en el dominio; pruebas de integración con TestClient; pruebas de contrato para workers.
¿Flags de características?
Flags de entorno pydantic-settings; migrar a proveedor cuando se necesiten toggles de PM.
¿Versionado de API?
Prefijo de URL /v1; los cambios que rompen obtienen /v2 con política de puesta fuera de servicio.
¿Secretos?
SSM/Secretos Externos; nunca en capas de imagen.
¿Limitación de tasa?
Middleware de bucket de tokens Redis por nivel de inquilino.
¿Comparación con la referencia de Node?
Misma disciplina de monolito modular; Python gana en adyacencia de datos/ML.
Relacionado
- Referencia: Un Pipeline de ML de Producción - Adyacencia de ML
- Antes/Después: Migración de Síncrono a Asíncrono - Camino de escalado
- Conceptos Básicos de FastAPI - Profundidad del framework
- Dockerizando Python - Construcción de imágenes
- Mejores Prácticas de Entrega - Envío seguro
Versiones de la Pila Tecnológica: Esta página fue escrita para Python 3.14.0 (estable 3.14, mantenimiento 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, y uv 0.6+.