Conocimientos previos

Bases de datos para SaaS: Postgres + Drizzle en términos para principiantes

Guía para principiantes sobre PostgreSQL y Drizzle ORM en SaaS: tablas, filas y relaciones, por qué Postgres, esquema tipado en TypeScript, consultas, índices, seguridad de datos, migraciones y diferencias entre local y producción.

Bases de datos para SaaS: Postgres + Drizzle en términos para principiantes

Al construir una app SaaS, necesitas un lugar donde guardar datos: usuarios, pedidos, posts, etc. Ahí entra la base de datos. En este artículo desmitificamos las bases de datos usando PostgreSQL (Postgres) como elección por defecto y mostramos cómo usar el ORM Drizzle en TypeScript para interactuar con Postgres de forma amigable para principiantes. Veremos qué son tablas y filas (sin jerga), por qué Postgres es una gran opción, cómo Drizzle te permite definir y consultar el esquema con facilidad, y lo básico sobre consultas, índices y cuidado de tus datos. También explicamos la diferencia entre usar una base de datos en desarrollo local y en producción, y resolvemos dudas típicas como “¿necesito SQL?” y “¿qué pasa si cambio una columna?”. Al final deberías entender el papel de la base de datos en tu SaaS y sentirte cómodo ejecutando migraciones para evolucionar tu esquema. ¡Vamos!


Tablas, filas y relaciones (sin jerga)

Piensa en una tabla de base de datos como en una hoja de cálculo. Tiene columnas (campos) y filas (entradas):

  • Tabla: colección de datos organizada en filas y columnas —por ejemplo, una tabla users.
  • Fila: una entrada de la tabla. En users, una fila representa una cuenta de usuario (los datos de una persona).
  • Columna: una pieza de información por fila, con nombre y tipo. En users podrías tener email, password, created_at, etc. Cada fila tendrá valores para esas columnas.
  • Clave primaria (Primary Key): columna(s) que identifican de forma única cada fila (el “ID” único del registro).
  • Relación: conexión entre tablas. Por ejemplo, si tienes sessions para sesiones de login, podría tener user_id que referencia una fila en users. Así sabes a qué usuario pertenece la sesión. Esta vinculación de filas es la esencia de las bases de datos relacionales.

En español llano: una tabla es como Excel; cada fila es un registro (un usuario, un pedido…), cada columna es una propiedad (email, importe, fecha…), y las tablas se enlazan mediante IDs (un pedido enlaza al usuario que lo hizo).


Por qué PostgreSQL es un gran valor por defecto

Al elegir base de datos para un SaaS, PostgreSQL suele recomendarse como un gran valor por defecto [1]:

  • Fiabilidad probada: Postgres lleva décadas y es sinónimo de robustez. Cumple ACID íntegramente (maneja transacciones de forma segura: no “pierde” dinero a mitad de una operación).
  • Propósito general y potente: no es nicho. Sirve para cuentas de usuario, posts, transacciones financieras… Además tiene funciones avanzadas (columnas JSON, búsqueda full‑text, GIS…) si las necesitas luego.
  • Escala bien: puedes empezar pequeño y crecer. Muchas startups empiezan con una instancia y escalan a millones de usuarios con buen tuning y hardware [1].
  • Gran ecosistema y soporte: por su popularidad hay múltiples opciones gestionadas (incluso tiers gratis): Heroku Postgres, Neon, Supabase, Railway, AWS RDS, etc. [2] y una comunidad enorme.
  • Open source y coste efectivo: es libre y sin licencias. Incluso los servicios gestionados suelen ser asequibles.
  • Compatibilidad: muchas bases (Redshift, CockroachDB, TimescaleDB) hablan “dialecto Postgres” o se basan en él [2].

En resumen, Postgres es una base confiable y flexible. “Postgres es un gran default” por una razón: cubre muchos casos y tiene un historial de éxito [1].


Introducción a Drizzle ORM: esquema tipado en TypeScript

Ya tenemos base de datos; ¿cómo hablamos con ella desde el código? En lugar de escribir SQL crudo a mano, usamos un ORM (Object‑Relational Mapper). Drizzle es un ORM moderno para TypeScript que ofrece un enfoque tipado para bases SQL como Postgres. Define el esquema en TypeScript: describes tablas y columnas en código y Drizzle lo usa como fuente de verdad para consultas y migraciones [3]:

  • Fuente única de verdad: el esquema en un solo sitio (código). Drizzle valida consultas y genera migraciones a partir de él.
  • Tipado: al estar en TS, obtienes autocompletado y chequeo de tipos. Si consultas una columna inexistente, no compila.
  • Sin “cadenas mágicas”: en vez de SQL como string, usas funciones/constantes verificables.

Ejemplo de tabla sessions para sesiones de login:

import { pgTable, varchar, text, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core";

export const sessions = pgTable(
  "sessions",
  {
    id: varchar({ length: 255 }).primaryKey(),          // Clave primaria (ID) tipo string
    user_id: varchar({ length: 255 }).notNull(),        // ID del usuario dueño de la sesión (obligatorio)
    token: varchar({ length: 512 }).notNull(),          // Token único de la sesión (obligatorio)
    expires_at: timestamp({ withTimezone: true }).notNull(), // Cuándo expira la sesión
    ip_address: varchar({ length: 255 }),               // (Opcional) IP de la sesión
    user_agent: text(),                                 // (Opcional) User‑Agent del navegador/dispositivo
    created_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
    updated_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    // Índices y restricciones:
    uniqueIndex("sessions_token_unique_idx").on(table.token),  // Garantiza tokens únicos
    index("sessions_user_id_idx").on(table.user_id),           // Búsquedas rápidas por user_id
  ]
);

En llano:

  • pgTable("sessions", {...}, (table) => [...]) define la tabla sessions.
  • Columnas:
    • id es la PK (varchar(255)).
    • user_id es string obligatorio y relaciona con users.id.
    • token string obligatorio, que queremos único.
    • expires_at timestamp con zona horaria obligatorio.
    • ip_address y user_agent son opcionales.
    • created_at/updated_at llevan defaultNow().
  • Tercer argumento: índices/restricciones (único por token; índice por user_id para rendimiento).

Definir el esquema en código lo hace visible y versionable; Drizzle lo usa para migraciones.

¿Por qué Drizzle en Next.js/TS? Porque abraza TS: tipos generados, chequeo en compilación y un motor liviano que traduce a SQL de forma transparente (ideal incluso para entornos serverless).


Consultar datos con Drizzle (más índices y seguridad de datos)

Definir tablas es la mitad; también hay que consultar y modificar. Drizzle ofrece una API fluida para select/insert/update/delete de forma tipada. Ejemplo: obtener créditos (transacciones) de un usuario y ordenarlos por fecha:

SELECT *
FROM credits
WHERE user_uuid = 'some-user-id'
ORDER BY created_at DESC;

En Drizzle:

import { db } from "@/db";                      // instancia de Drizzle
import { credits as creditsTable } from "@/db/schema";  // esquema de la tabla credits

const userId = "some-user-uuid";
const userCredits = await db.select().from(creditsTable)
  .where(creditsTable.user_uuid.eq(userId))
  .orderBy(creditsTable.created_at.desc());

Notas:

  • creditsTable.user_uuid.eq(userId) evita inyecciones (consulta parametrizada) y errores de tipeo.
  • orderBy(...desc()) ordena descendente.
  • El resultado es un array con el tipo correcto en TS según el esquema.

Índices, restricciones y seguridad:

  • Índices para rendimiento: añade índices a columnas por las que filtras o unes (foreign keys como user_id). Sin índice, las búsquedas se degradan al crecer la tabla.
  • Restricciones UNIQUE: integridad de datos (email único, token único, etc.). La BD rechaza duplicados.
  • NOT NULL: columnas obligatorias para evitar registros incompletos.
  • Tipos de datos: la BD hace cumplir tipos (no puedes guardar “Mañana” en un timestamp). TS te guía en inserts/updates.
  • Transacciones: para operaciones que deben “todo o nada” (mención breve).

Base de datos de desarrollo local vs de producción

Trabajar con una BD local (tu portátil) difiere de gestionarla en producción (app viva con usuarios reales). Distingue y trata cada una correctamente:

BD de desarrollo (local):

  • Suele ejecutarse en tu máquina o Docker. Solo para construir/probar.
  • Tienes libertad para experimentar: datos de prueba, resets, operaciones destructivas sin consecuencias graves.
  • Conexión en .env (p. ej., DATABASE_URL=postgres://postgres:password@localhost:5432/myapp_dev).
  • En equipo: cada dev puede tener su BD local. No contiene datos reales.
  • Rendimiento: una BD local es pequeña; una consulta instantánea en dev podría ser lenta en prod —usa índices y buenas prácticas desde temprano.

BD de producción:

  • Es la real: guarda datos de usuarios. Trátala con máximo cuidado: no “experimentes” ahí.
  • Suele ser un servicio gestionado (Heroku, AWS, GCP, Azure, Supabase, Neon…) con backups, updates y escalado.
  • Usa otra cadena de conexión (env vars seguras en el servidor). Nunca reutilices la local.
  • Migraciones en prod: prueba antes en dev y aplica durante el deploy cuando el código ya soporta los cambios [5].
  • Volumen y monitorización: vigila rendimiento (consultas lentas, uso de índices). Activa logs/insights del proveedor.
  • Backups y seguridad: habilita copias automáticas; restringe el acceso de red; contraseñas fuertes y parches al día.

En corto: una BD para dev (segura para iterar) y otra para prod (sagrada: protégela, haz backups y aplica cambios con cabeza). Nunca apuntes tu app local a la BD de producción.


Preguntas comunes de principiantes

P: ¿Necesito aprender SQL para usar Postgres y Drizzle?

R: No de inmediato, pero ayuda mucho a largo plazo. Un ORM como Drizzle te permite empezar sin escribir SQL crudo para cada operación. Aun así, entender fundamentos relacionales y algo de SQL te hará mejor desarrollador y te ayudará a optimizar o depurar [4]:

  • Modelado de datos: diseño de tablas y relaciones.
  • Consultas simples: SELECT, INSERT, UPDATE, DELETE, WHERE.
  • Joins: combinar datos relacionados.
  • Agregaciones: contar, sumar, etc.

Piensa en el ORM como “ruedas auxiliares”: puedes avanzar sin dominar SQL, pero conocerlo te prepara para cualquier terreno.

P: ¿Qué pasa si cambio una columna o tabla más adelante? (¿Por qué importan las migraciones?)

R: El cambio es constante. Si modificas el esquema en el código de Drizzle, tu app esperará esa estructura; la BD real no cambiará hasta aplicar una migración. Sin migraciones, el código y la BD divergen y habrá errores.

Migraciones con Drizzle Kit [5]:

  1. Editas el esquema en TypeScript.
  2. Generas la migración (drizzle-kit generate), que compara con el snapshot anterior y crea SQL de cambios.
  3. Revisa diferencias (renombres vs columnas nuevas) y confirma cuando proceda.
  4. Aplicas la migración (drizzle-kit migrate) sobre la BD.
  5. Se actualiza el snapshot y comiteas el archivo de migración.

Ejemplo práctico: cambiar credits de INT a BIGINT. Cambiar solo el esquema no modifica la columna existente; la migración generará el ALTER TABLE ... necesario. En cambios incompatibles quizá necesites pasos manuales. En prod, respalda antes, aplica durante el deploy y, si es posible, escribe migraciones reversibles.


Siguientes pasos: pruébalo con migraciones de Drizzle (CTA)

Ahora que entiendes Postgres y Drizzle, toca practicar. En el contexto de tu proyecto (plantilla o starter), probablemente ya haya esquema y scripts listos. Sigue el README para generar y aplicar migraciones en tu BD local, por ejemplo:

npm run db:generate   # genera una migración según diferencias de esquema
npm run db:migrate    # aplica la migración a la base de datos

Abre el archivo generado (normalmente en drizzle/ o migrations/) y revisa el SQL. Después, inserta datos de prueba con Drizzle, consúltalos y, si te animas, añade una nueva columna al esquema y repite la generación/aplicación. Así interiorizas el ciclo de migraciones.

¡Felicidades! Diste un gran paso en bases de datos para SaaS: fundamentos relacionales (tablas, filas y relaciones), por qué confiar en Postgres, cómo aprovechar Drizzle y la importancia de las migraciones. Con esto, estás preparado para construir sobre una base sólida de datos.


Referencias