SincroDev Logo SincroDev

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:

  1. modelar entidades
  2. persistir en PostgreSQL
  3. exponer endpoints claros
  4. 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:

  • id
  • name
  • email
  • message
  • status
  • created_at
  • updated_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:

  • TEXT para mensajes largos
  • status acotado 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/requests
  • GET /api/requests
  • GET /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:

  1. definir una entidad clara
  2. modelarla en PostgreSQL
  3. diseñar pocos endpoints pero buenos
  4. validar entrada con Pydantic
  5. separar persistencia de lógica
  6. 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í.