Backend con FastAPI y PostgreSQL para un sistema interno
Muchas herramientas internas empiezan igual:
- una planilla
- después dos
- luego un formulario
- después un archivo JSON
- y finalmente una cadena de parches que nadie quiere tocar
Mientras el equipo es chico, eso aguanta.
Pero en cuanto el proceso se vuelve importante, aparece el problema real: ya no necesitas “otro formulario”, necesitas una base de sistema.
En este tutorial voy a mostrar cómo pensar y montar esa base usando:
- FastAPI para la API
- PostgreSQL para persistencia
- una estructura simple pero seria para crecer sin rehacer todo a los dos meses
Qué vas a construir
Vamos a modelar un caso sencillo pero realista:
un sistema interno para registrar y gestionar solicitudes o contactos operativos.
La arquitectura mínima será esta:
Cliente interno / frontend
↓
API FastAPI
↓
Validación y lógica de negocio
↓
PostgreSQL
No es un ERP. No es un CRM completo. Es una base limpia sobre la que luego puedes construir panel, autenticación, automatizaciones o reporting.
Cuándo conviene este enfoque
Tiene sentido cuando:
- un proceso ya no entra cómodamente en planillas
- necesitas trazabilidad y consistencia
- varias personas editan o consultan el mismo flujo
- quieres una API propia para crecer con criterio
No conviene cuando:
- el problema todavía es demasiado pequeño
- una automatización simple resuelve el 80%
- no tienes claro qué proceso exacto quieres ordenar
Regla práctica: primero claridad operativa, después software.
El punto de partida real
En este proyecto, el backend actual en backend/main.py guarda contactos en un archivo JSON. Eso es útil para un caso mínimo.
Pero si quisieras convertir ese flujo en una base de sistema interno, el siguiente paso razonable sería:
- modelar entidades
- persistir en PostgreSQL
- exponer endpoints claros
- separar validación de almacenamiento
Este tutorial cubre justamente esa transición conceptual.
Paso 1: Definir la entidad principal
No empieces por la base de datos. Empieza por el proceso.
Para este ejemplo, supongamos que la entidad central es Solicitud.
Campos mínimos:
idnameemailmessagestatuscreated_atupdated_at
Eso ya te permite:
- registrar entradas
- listarlas
- cambiar estado
- auditar cuándo se crearon
Paso 2: Diseñar la tabla en PostgreSQL
Una versión inicial razonable podría ser:
CREATE TABLE requests (
id SERIAL PRIMARY KEY,
name VARCHAR(150) NOT NULL,
email VARCHAR(255) NOT NULL,
message TEXT NOT NULL,
status VARCHAR(30) NOT NULL DEFAULT 'new',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
Observaciones importantes:
TEXTpara mensajes largosstatusacotado a un conjunto pequeño- timestamps desde el día uno
Aunque parezca básico, esta diferencia contra un JSON ya es enorme:
- puedes consultar
- filtrar
- paginar
- ordenar
- mantener consistencia
Paso 3: Definir el contrato de la API
Antes de programar, decide qué operaciones necesita realmente el sistema.
Para una primera versión:
POST /api/requestsGET /api/requestsGET /api/requests/{id}PATCH /api/requests/{id}
Eso cubre creación, listado, detalle y cambio de estado.
No construyas 20 endpoints si solo necesitas 4.
Paso 4: Modelos Pydantic de entrada y salida
FastAPI brilla cuando dejas claro el contrato.
Ejemplo:
from datetime import datetime
from pydantic import BaseModel, EmailStr
class RequestCreate(BaseModel):
name: str
email: EmailStr
message: str
class RequestUpdate(BaseModel):
status: str
class RequestResponse(BaseModel):
id: int
name: str
email: EmailStr
message: str
status: str
created_at: datetime
updated_at: datetime
Esto ya te da varias ventajas:
- validación temprana
- documentación automática
- contratos más estables
Paso 5: Conectar FastAPI con PostgreSQL
Puedes hacerlo con varias combinaciones. Una base práctica y común sería:
- SQLAlchemy
- psycopg
- Alembic para migraciones
Configuración conceptual:
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
DATABASE_URL = "postgresql+psycopg://app:secret@db:5432/appdb"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
Y una dependencia típica para FastAPI:
from collections.abc import Generator
def get_db() -> Generator:
db = SessionLocal()
try:
yield db
finally:
db.close()
La idea es simple: abrir sesión por request y cerrarla siempre.
Paso 6: Crear el modelo ORM
Ejemplo mínimo:
from sqlalchemy import DateTime, Integer, String, Text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase):
pass
class Request(Base):
__tablename__ = "requests"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(String(150), nullable=False)
email: Mapped[str] = mapped_column(String(255), nullable=False)
message: Mapped[str] = mapped_column(Text, nullable=False)
status: Mapped[str] = mapped_column(String(30), default="new", nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
Con eso ya tienes una entidad seria para trabajar.
Paso 7: Endpoint de creación
La lógica mínima del POST sería:
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
router = APIRouter()
@router.post("/api/requests", response_model=RequestResponse)
def create_request(payload: RequestCreate, db: Session = Depends(get_db)):
if len(payload.message.strip()) < 10:
raise HTTPException(status_code=400, detail="El mensaje es muy corto")
item = Request(
name=payload.name.strip(),
email=payload.email.strip(),
message=payload.message.strip(),
)
db.add(item)
db.commit()
db.refresh(item)
return item
Fíjate en algo importante:
- validas entrada
- normalizas
- persistes
- devuelves una representación clara
Ese patrón te evita mucha deuda desde el principio.
Paso 8: Endpoint de listado
Un listado razonable no debería devolver “todo para siempre”.
Mejor empezar ya con paginación:
@router.get("/api/requests", response_model=list[RequestResponse])
def list_requests(
db: Session = Depends(get_db),
limit: int = 50,
offset: int = 0,
):
return (
db.query(Request)
.order_by(Request.created_at.desc())
.offset(offset)
.limit(limit)
.all()
)
Aunque tu sistema tenga pocos registros hoy, diseñar con paginación desde el día uno suele evitar reescrituras innecesarias.
Paso 9: Endpoint de actualización de estado
En muchos sistemas internos, lo importante no es solo registrar datos sino moverlos entre estados.
Ejemplo:
VALID_STATUSES = {"new", "in_progress", "resolved", "archived"}
@router.patch("/api/requests/{request_id}", response_model=RequestResponse)
def update_request(
request_id: int,
payload: RequestUpdate,
db: Session = Depends(get_db),
):
item = db.get(Request, request_id)
if not item:
raise HTTPException(status_code=404, detail="Solicitud no encontrada")
if payload.status not in VALID_STATUSES:
raise HTTPException(status_code=400, detail="Estado inválido")
item.status = payload.status
db.commit()
db.refresh(item)
return item
Esto parece menor, pero es la base de casi todos los backoffices reales:
- crear
- listar
- cambiar estado
- seguir historial operativo
Paso 10: Qué deberías separar desde el principio
Aunque el sistema sea pequeño, conviene separar:
- esquemas de entrada/salida
- acceso a base
- lógica de negocio
- routers
No necesitas una arquitectura barroca, pero sí evitar un main.py infinito con todo mezclado.
Una estructura razonable:
backend/
├── main.py
├── db.py
├── models.py
├── schemas.py
├── routers/
│ └── requests.py
└── services/
└── requests.py
Errores comunes al construir sistemas internos
1) Modelar la base según la UI actual
La interfaz cambia más rápido que el dominio. Diseña primero el proceso, no la pantalla.
2) Meter toda la lógica en el endpoint
Si cada endpoint hace validación, reglas, persistencia y formateo todo junto, mantenerlo se vuelve caro muy rápido.
3) No versionar cambios de esquema
Sin migraciones, cada cambio en base de datos se vuelve una operación manual con riesgo.
4) No pensar en estados
Muchos sistemas internos no fallan por falta de campos, sino por falta de flujo: nadie sabe si algo está nuevo, en revisión o resuelto.
5) No decidir quién puede ver o editar qué
Aunque hoy seas el único usuario, si el sistema crece necesitarás roles y permisos.
Qué añadir después de esta base
Una vez que la base funciona, los siguientes pasos naturales son:
- autenticación
- roles y permisos
- filtros por estado
- búsqueda
- auditoría
- automatizaciones sobre eventos
- panel interno para operación
Es importante el orden:
primero consistencia de modelo y API, luego capas extra.
Cuándo usar esto y cuándo no
Usa este enfoque cuando de verdad estés construyendo una pieza propia del negocio.
No lo uses si el problema real se resolvía con:
- un formulario simple
- una automatización puntual
- una herramienta ya existente bien configurada
Construir backend a medida tiene sentido cuando el proceso lo justifica.
Resumen
Para pasar de un flujo frágil a una base seria de sistema interno, necesitas:
- definir una entidad clara
- modelarla en PostgreSQL
- diseñar pocos endpoints pero buenos
- validar entrada con Pydantic
- separar persistencia de lógica
- pensar en estados y evolución desde el principio
No hace falta construir una plataforma gigante para empezar bien. Hace falta evitar la arquitectura accidental.
Y ahí FastAPI + PostgreSQL sigue siendo una combinación muy difícil de superar para sistemas internos: simple, expresiva y suficientemente robusta para crecer.
Si necesitas diseñar un sistema interno propio, ordenar un backend que ya empezó a deformarse o bajar un proceso operativo a una API mantenible, puedes escribirme desde Sobre Mí.