Conocimientos previos

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

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

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

Al construir un SaaS necesitas almacenar datos: usuarios, pedidos, posts, sesiones, etc. Esta guía explica bases de datos usando PostgreSQL (Postgres) como elección por defecto y muestra cómo Drizzle ORM te permite definir y consultar el esquema con TypeScript.

Aprenderás:

  • Qué son tablas, filas, columnas y relaciones.
  • Por qué Postgres es un gran default.
  • Cómo Drizzle define esquemas y consultas tipadas.
  • Por qué índices y constraints mantienen datos rápidos y limpios.
  • Diferencias entre local y producción.
  • Qué son las migraciones y por qué importan.

Tablas, filas y relaciones (sin jerga)

Piensa en una tabla como una hoja de cálculo:

  • Tabla: colección de datos (por ejemplo users).
  • Fila: una entrada (un usuario).
  • Columna: un dato por fila (email, created_at, plan).
  • Primary key: ID único por fila.
  • Relación: un vínculo entre tablas, normalmente por ID.

Ejemplo: una tabla sessions puede incluir user_id que apunta a una fila de users. Esa es la relación. Por eso Postgres es relacional.

En simple: las tablas guardan tipos de cosas, las filas guardan cosas individuales y las relaciones conectan cosas.


Por qué PostgreSQL es un gran default

Postgres se recomienda mucho como opción por defecto [1] porque es:

  • Confiable: décadas de uso y transacciones ACID.
  • Generalista: sirve para SaaS, finanzas, contenido y más.
  • Escalable: puedes empezar pequeño y crecer [1].
  • Bien soportado: muchas opciones de hosting gestionado [2].
  • Open source: sin lock‑in ni licencias.
  • Compatible: otros sistemas hablan “Postgres” [2].

Salvo que tengas un caso muy especial, Postgres es una base sólida para un SaaS.


Drizzle ORM: esquema tipado en TypeScript

Un ORM (Object‑Relational Mapper) te permite hablar con la DB desde tu lenguaje en lugar de escribir SQL crudo. Drizzle es un ORM moderno en TypeScript que mantiene SQL cercano, con seguridad de tipos.

Por qué Drizzle es amigable:

  • Fuente única de verdad: defines el esquema en TypeScript y lo reutilizas en consultas y migraciones [3].
  • Type safety: si un campo no existe, TypeScript avisa.
  • Sin strings mágicos: construyes queries con funciones, no con strings.

Ejemplo de esquema en Drizzle (sessions):

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

export const sessions = pgTable(
  "sessions",
  {
    id: varchar({ length: 255 }).primaryKey(),          // Primary key column, a string ID
    user_id: varchar({ length: 255 }).notNull(),        // ID of the user who owns this session (must have a value)
    token: varchar({ length: 512 }).notNull(),          // A unique session token (must have a value)
    expires_at: timestamp({ withTimezone: true }).notNull(), // When the session expires
    ip_address: varchar({ length: 255 }),               // (Optional) IP address of the session
    user_agent: text(),                                 // (Optional) User agent string of the browser/device
    created_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
    updated_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
  },
  (table) => [
    // Indexes and constraints:
    uniqueIndex("sessions_token_unique_idx").on(table.token),  // Ensure no two sessions have the same token
    index("sessions_user_id_idx").on(table.user_id),           // Index to quickly look up sessions by user_id
  ]
);

En términos simples:

  • sessions es el nombre de la tabla.
  • id, user_id, token, expires_at son obligatorios.
  • ip_address y user_agent son opcionales.
  • created_at y updated_at se rellenan automáticamente.
  • Índice único en token evita duplicados.
  • Índice en user_id acelera consultas.

El esquema en TypeScript permite detectar errores temprano y sirve como base para migraciones.


Consultar con Drizzle (índices y seguridad)

Drizzle ofrece una API fluida para select/insert/update/delete y genera SQL seguro.

Ejemplo SQL:

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

Ejemplo Drizzle:

import { db } from "@/db";                      // our Drizzle database instance
import { credits as creditsTable } from "@/db/schema";  // the credits table schema we defined

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

Por qué es más seguro:

  • Type safety: nombres de columnas validados en compilación.
  • Queries parametrizadas: reduce riesgo de SQL injection.
  • Intención clara: el código refleja la consulta.

Índices, constraints y seguridad básica:

  • Índices: aceleran búsquedas frecuentes.
  • Constraints únicas: evitan duplicados (email único, token único).
  • NOT NULL: evita datos incompletos.
  • Tipos de datos: Postgres hace cumplir tipos; Drizzle los refleja.
  • Transacciones: agrupan operaciones para éxito/fracaso conjunto.
  • Backups: habilítalos en producción.

Base local vs base de producción

Desarrollo local:

  • Corre en tu laptop o Docker.
  • Puedes resetear sin riesgo.
  • Usa DATABASE_URL en .env.
  • Datos de prueba.

Producción:

  • Datos reales; no experimentes aquí.
  • DB gestionada (Neon, Supabase, RDS, etc.).
  • Credenciales seguras, backups, monitoreo.
  • Migraciones cuidadosas al desplegar [5].

Clave: separa dev y prod, y prueba migraciones en local antes de producción.


Preguntas comunes

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

R: No al principio, pero ayuda mucho a largo plazo. Un ORM evita escribir SQL crudo, pero SQL es útil para debug y performance [4].

Bases útiles:

  • Modelado de datos: tablas y relaciones.
  • Consultas básicas: SELECT, INSERT, UPDATE, DELETE, WHERE.
  • Joins: combinar datos relacionados.
  • Agregaciones: contar, sumar, promediar.

Drizzle no oculta SQL; te ayuda a usarlo de forma segura. Piensa en el ORM como rueditas: te acelera, pero SQL te hace más fuerte.

P: ¿Qué pasa si cambio una columna o tabla? (migraciones)

Cuando cambias el esquema en código, la base real no se actualiza sola. Las migraciones sincronizan ambos.

Cómo funcionan con Drizzle:

  1. Actualiza el esquema en TypeScript.
  2. Ejecuta drizzle-kit generate para crear migraciones [5].
  3. Revisa la migración (Drizzle puede preguntar si es rename).
  4. Aplica con drizzle-kit migrate.
  5. Versiona los archivos de migración.

Ejemplo: cambiar credits de INT a BIGINT requiere migración. En producción, haz backup y aplica migraciones en el deploy.


Siguientes pasos: prueba migraciones

Sigue el README de tu proyecto para generar y aplicar migraciones. Por ejemplo:

npm run db:generate   # generates a migration based on schema differences
npm run db:migrate    # applies the migration to the database

Luego prueba:

  • Insertar un dato de prueba.
  • Consultarlo con Drizzle.
  • Añadir una columna y volver a migrar.

Así verás cómo el esquema en TS se convierte en SQL y mantiene tu DB alineada.


Referencias