Saltar al contenido
Lección 12 de 22

Bases de Datos para Vibe Coders

14 min read

Cuándo necesitas una base de datos

No todos los proyectos necesitan una base de datos desde el día uno. Pero en el momento en que tu app necesita recordar algo después de que el usuario cierra el navegador, necesitas almacenamiento persistente.

Estas son las señales:

  • Los usuarios crean cuentas y esperan que sus datos estén ahí cuando vuelvan
  • Estás almacenando contenido que los usuarios generan — publicaciones, tareas, mensajes, archivos
  • Necesitas buscar, filtrar u ordenar datos
  • Múltiples usuarios necesitan ver los mismos datos
  • Necesitas rastrear historial o cambios a lo largo del tiempo

Una base de datos es una forma estructurada de almacenar, recuperar y gestionar datos. No es un archivo lleno de JSON (aunque puede sentirse así cuando estás empezando). Es un sistema diseñado para acceso concurrente, integridad de datos y consultas eficientes.

Tipos de bases de datos

Bases de datos relacionales (PostgreSQL, MySQL, SQLite) almacenan datos en tablas con filas y columnas. Las tablas pueden referenciarse entre sí a través de relaciones. Usan SQL (Structured Query Language) para consultas. Esta es la opción por defecto para la mayoría de aplicaciones web.

Bases de datos de documentos (MongoDB, Firebase Firestore) almacenan datos como documentos flexibles tipo JSON. Son más fáciles para empezar, pero más difíciles de mantener a medida que tus datos se vuelven más complejos e interconectados.

Para este curso, nos enfocamos en bases de datos relacionales porque son el estándar de la industria para apps web, funcionan de maravilla con Prisma, y la IA genera excelente SQL y schemas relacionales.

SQLite vs. PostgreSQL

Estas son las dos bases de datos que encontrarás con más frecuencia en el desarrollo web moderno. Cada una tiene un caso de uso claro.

SQLite: La base de datos sin configuración

SQLite almacena toda tu base de datos en un solo archivo. Sin servidor que instalar, sin configuración, sin puertos que gestionar. Está integrada en cada smartphone y es el motor de base de datos más ampliamente desplegado en el mundo.

Ideal para:

  • Desarrollo local y prototipado
  • Apps pequeñas a medianas con un solo servidor
  • Aplicaciones embebidas
  • Aprendizaje y experimentación

Limitaciones:

  • Un solo escritor a la vez (sin escrituras concurrentes desde múltiples servidores)
  • No soporta algunas características avanzadas de SQL
  • No ideal para apps de producción con mucho tráfico

PostgreSQL: El estándar de producción

PostgreSQL es una base de datos cliente-servidor con todas las funcionalidades. Se ejecuta como un proceso (o servicio) separado y maneja conexiones concurrentes, consultas complejas, búsqueda de texto completo, almacenamiento JSON y mucho más.

Ideal para:

  • Aplicaciones web de producción
  • Apps con múltiples servidores o funciones serverless
  • Consultas y relaciones complejas
  • Apps que necesitan búsqueda de texto completo o columnas JSON

Guía de decisión

¿Empezando un nuevo proyecto? Usa SQLite en desarrollo por velocidad y simplicidad. Planea cambiar a PostgreSQL para producción. Prisma hace este cambio prácticamente indoloro — cambias una línea en tu configuración.

Desarrollo:  SQLite       → rapido, cero configuracion, funciona en cualquier lugar
Produccion:  PostgreSQL   → escalable, concurrente, completo en funcionalidades

Prisma ORM: Tu capa de base de datos amigable con IA

Un ORM (Object-Relational Mapping) te permite trabajar con tu base de datos usando tu lenguaje de programación en lugar de escribir SQL crudo. Prisma es el mejor ORM para vibe coding porque:

  1. Schema declarativo — describes tu modelo de datos en un archivo de schema legible, y Prisma genera todo lo demás
  2. Consultas type-safe — TypeScript conoce la forma de tus datos, así que obtienes autocompletado y verificación de errores
  3. API legibleprisma.user.findMany() se lee como inglés
  4. Amigable con IA — el formato de schema y la API de consultas de Prisma están bien representados en los datos de entrenamiento de la IA

Cuando le dices a la IA "crea un usuario con email y contraseña," y estás usando Prisma, la IA genera código limpio y type-safe que funciona al primer intento.

Configurando Prisma

Instalación

npm install prisma @prisma/client
npx prisma init

Esto crea un directorio prisma/ con un archivo schema.prisma y un archivo .env para la URL de tu base de datos.

Configurando el datasource

Para SQLite (desarrollo):

// prisma/schema.prisma
datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

generator client {
  provider = "prisma-client-js"
}

Para PostgreSQL (producción):

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

El env("DATABASE_URL") lee de tu archivo .env:

# .env
DATABASE_URL="postgresql://user:password@localhost:5432/myapp"

El archivo de schema

El archivo schema.prisma es la única fuente de verdad para la estructura de tu base de datos. Todo — tablas, columnas, relaciones, índices — se define aquí. Prisma usa este archivo para generar el cliente de base de datos, crear migraciones y proporcionar seguridad de tipos.

Diseño de schemas con IA

Aquí es donde el vibe coding brilla. Describes tu modelo de datos en lenguaje natural, y la IA crea el schema de Prisma. Así es como debes pensarlo.

Describiendo tus datos a la IA

Prompt: "Crea un schema de Prisma para una plataforma de blog. Los usuarios pueden crear publicaciones. Las publicaciones pueden tener múltiples etiquetas. Los usuarios pueden comentar en publicaciones. Cada usuario tiene un perfil con bio y URL de avatar."

La IA traduce esto en un schema:

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String
  password  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  profile   Profile?
  posts     Post[]
  comments  Comment[]
}

model Profile {
  id        String  @id @default(cuid())
  bio       String?
  avatarUrl String?
  userId    String  @unique
  user      User    @relation(fields: [userId], references: [id], onDelete: Cascade)
}

model Post {
  id          String   @id @default(cuid())
  title       String
  content     String
  published   Boolean  @default(false)
  createdAt   DateTime @default(now())
  updatedAt   DateTime @updatedAt

  authorId    String
  author      User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  comments    Comment[]
  tags        Tag[]
}

model Comment {
  id        String   @id @default(cuid())
  content   String
  createdAt DateTime @default(now())

  authorId  String
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  postId    String
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
}

model Tag {
  id    String @id @default(cuid())
  name  String @unique
  posts Post[]
}

Desglosemos los tipos de relación que usa este schema.

Relaciones uno a muchos

Un usuario tiene muchas publicaciones. Una publicación pertenece a un usuario:

model User {
  posts Post[]    // Un usuario → muchas publicaciones
}

model Post {
  authorId String
  author   User   @relation(fields: [authorId], references: [id])
}

El campo authorId almacena la clave foránea. El campo author es una relación virtual — no crea una columna pero te permite navegar entre registros.

Relaciones muchos a muchos

Las publicaciones pueden tener muchas etiquetas. Las etiquetas pueden pertenecer a muchas publicaciones:

model Post {
  tags Tag[]
}

model Tag {
  posts Post[]
}

Prisma crea una tabla de unión implícita detrás de escena. No necesitas definirla tú mismo (aunque puedes hacerlo si necesitas campos adicionales en la relación).

Relaciones uno a uno

Un usuario tiene exactamente un perfil:

model User {
  profile Profile?   // Opcional: el usuario podria no tener perfil aun
}

model Profile {
  userId String @unique   // @unique impone uno a uno
  user   User   @relation(fields: [userId], references: [id])
}

La restricción @unique en userId asegura que cada usuario puede tener como máximo un perfil.

Índices para rendimiento

A medida que tus datos crecen, las consultas se vuelven más lentas. Los índices aceleran las búsquedas en columnas específicas:

model Post {
  authorId  String
  createdAt DateTime @default(now())

  @@index([authorId])
  @@index([createdAt])
  @@index([authorId, createdAt])  // Indice compuesto
}

Prompt: "Agrega índices al modelo Post para consultas comunes: encontrar publicaciones por autor, listar publicaciones recientes, y encontrar publicaciones por autor ordenadas por fecha."

Flujo de migraciones

Las migraciones son cambios versionados a tu schema de base de datos. Cuando modificas tu schema de Prisma, creas una migración que describe lo que cambió, y luego la aplicas a tu base de datos.

Flujo de trabajo en desarrollo

# Despues de cambiar schema.prisma:
npx prisma migrate dev --name add-post-status

Este comando:

  1. Compara tu schema con la base de datos actual
  2. Genera un archivo de migración SQL
  3. Aplica la migración a tu base de datos
  4. Regenera el cliente de Prisma

Despliegue a producción

npx prisma migrate deploy

Esto aplica todas las migraciones pendientes en orden. Nunca genera nuevas migraciones — solo ejecuta las existentes.

Por qué importan las migraciones

Las migraciones son un historial de los cambios en tu base de datos. Te permiten:

  • Rastrear qué cambió y cuándo
  • Avanzar (aplicar cambios) en nuevos entornos
  • Asegurar que todos los entornos tengan la misma estructura de base de datos
  • Colaborar con otros desarrolladores sin conflictos de schema

Nombra tus migraciones de forma significativa: add-user-roles, create-comments-table, add-post-published-field. Tu yo del futuro agradecerá a tu yo del presente.

Datos semilla

Los scripts de seed crean datos iniciales para desarrollo. Son esenciales para tener datos realistas con los que trabajar.

// prisma/seed.ts
import { PrismaClient } from "@prisma/client"

const prisma = new PrismaClient()

async function main() {
  // Crear usuarios
  const alice = await prisma.user.create({
    data: {
      email: "alice@example.com",
      name: "Alice Johnson",
      password: "hashed-password-here",
      profile: {
        create: { bio: "Desarrolladora full-stack y entusiasta del cafe" },
      },
    },
  })

  // Crear publicaciones con etiquetas
  await prisma.post.create({
    data: {
      title: "Getting Started with Prisma",
      content: "Prisma hace el acceso a bases de datos facil...",
      published: true,
      authorId: alice.id,
      tags: {
        connectOrCreate: [
          { where: { name: "prisma" }, create: { name: "prisma" } },
          { where: { name: "database" }, create: { name: "database" } },
        ],
      },
    },
  })

  console.log("Base de datos sembrada exitosamente")
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect())

Agrega a tu package.json:

{
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  }
}

Ejecuta con:

npx prisma db seed

Prompt: "Crea un script de seed con 5 usuarios, 20 publicaciones distribuidas entre ellos, etiquetas relevantes y comentarios de ejemplo."

Patrones comunes de consultas con Prisma

Estas son las operaciones que usarás todos los días. Aprende los patrones, y sabrás exactamente cómo darle prompts a la IA para operaciones de base de datos.

Crear

// Crear uno
const user = await prisma.user.create({
  data: { email: "bob@example.com", name: "Bob" },
})

// Crear muchos
const users = await prisma.user.createMany({
  data: [
    { email: "user1@example.com", name: "Usuario 1" },
    { email: "user2@example.com", name: "Usuario 2" },
  ],
})

Leer

// Buscar por campo unico
const user = await prisma.user.findUnique({
  where: { email: "alice@example.com" },
})

// Buscar muchos con condiciones
const posts = await prisma.post.findMany({
  where: { published: true, authorId: userId },
  orderBy: { createdAt: "desc" },
  take: 10,
  skip: 0,
})

// Incluir datos relacionados
const userWithPosts = await prisma.user.findUnique({
  where: { id: userId },
  include: { posts: true, profile: true },
})

Actualizar

// Actualizar uno
const updated = await prisma.post.update({
  where: { id: postId },
  data: { title: "Titulo Actualizado" },
})

// Actualizar muchos
await prisma.post.updateMany({
  where: { authorId: userId },
  data: { published: false },
})

// Upsert (crear si no existe, actualizar si existe)
const profile = await prisma.profile.upsert({
  where: { userId: userId },
  update: { bio: "Bio actualizada" },
  create: { userId: userId, bio: "Nueva bio" },
})

Eliminar

// Eliminar uno
await prisma.post.delete({ where: { id: postId } })

// Eliminar muchos
await prisma.comment.deleteMany({ where: { postId: postId } })

El onDelete: Cascade en tu schema maneja la eliminación automática de registros relacionados. Cuando eliminas un usuario, todas sus publicaciones y comentarios se eliminan también.

Filtrado, ordenamiento y paginación

const results = await prisma.post.findMany({
  where: {
    published: true,
    title: { contains: "prisma", mode: "insensitive" },
    createdAt: { gte: new Date("2026-01-01") },
    tags: { some: { name: "database" } },
  },
  orderBy: [{ createdAt: "desc" }, { title: "asc" }],
  skip: (page - 1) * pageSize,
  take: pageSize,
  include: { author: { select: { name: true, email: true } } },
})

Agregaciones

const stats = await prisma.post.aggregate({
  _count: true,
  _avg: { wordCount: true },
  where: { published: true },
})

const postsByAuthor = await prisma.post.groupBy({
  by: ["authorId"],
  _count: true,
  orderBy: { _count: { id: "desc" } },
})

Drizzle como alternativa

Prisma no es el único ORM. Drizzle ORM toma un enfoque diferente — es SQL-first, lo que significa que tu código TypeScript refleja de cerca el SQL que genera.

// Schema de Drizzle
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core"

export const posts = pgTable("posts", {
  id: text("id").primaryKey(),
  title: text("title").notNull(),
  published: boolean("published").default(false),
  createdAt: timestamp("created_at").defaultNow(),
})

// Consulta de Drizzle
const results = await db.select().from(posts).where(eq(posts.published, true))

Cuándo elegir Drizzle sobre Prisma:

  • Prefieres escribir código similar a SQL
  • Necesitas máximo rendimiento en consultas (Drizzle genera SQL más eficiente en algunos casos)
  • Quieres un runtime más ligero (Drizzle no tiene binario de query engine)

Cuándo elegir Prisma:

  • Quieres la opción más amigable para la IA (Prisma tiene más datos de entrenamiento)
  • Prefieres una API de nivel más alto y más abstracta
  • Valoras la GUI de Prisma Studio para explorar datos
  • Trabajas con un equipo que incluye desarrolladores que no conocen SQL

Ambos son excelentes. Para este curso, usamos Prisma porque la IA genera mejor código de Prisma de entrada.

Hospedaje de bases de datos

Necesitas un lugar donde alojar tu base de datos de producción. Aquí están las mejores opciones con tiers gratuitos:

Neon

PostgreSQL serverless. Tu base de datos escala a cero cuando está inactiva y se activa instantáneamente cuando se necesita. Perfecto para apps con tráfico impredecible.

  • Tier gratuito: 0.5 GB de almacenamiento, 190 horas de cómputo
  • Ideal para: Apps Next.js, arquitecturas serverless

Supabase

PostgreSQL con extras — autenticación integrada, suscripciones en tiempo real, almacenamiento y una API REST auto-generada. Es como Firebase pero con PostgreSQL.

  • Tier gratuito: 500 MB de almacenamiento, 2 proyectos
  • Ideal para: Apps que quieren un backend todo-en-uno

Turso

SQLite desplegado en el edge. Tu base de datos se ejecuta cerca de tus usuarios globalmente. Es SQLite para producción, usando un protocolo llamado libSQL.

  • Tier gratuito: 9 GB de almacenamiento, 500 bases de datos
  • Ideal para: Apps con muchas lecturas, edge computing

PlanetScale

MySQL serverless con un flujo de trabajo de branching similar a Git para cambios de schema. Cada branch es una base de datos aislada que puedes mergear.

  • Ideal para: Equipos que quieren cambios de schema seguros con migraciones basadas en branches

Prompt: "Configura el datasource de Prisma para usar Neon PostgreSQL. Configura el string de conexión para serverless con el parámetro ?sslmode=require."

Patrones de prompts para trabajo con bases de datos

Aquí hay prompts probados para tareas comunes de base de datos:

Diseño de schema: "Crea un schema de Prisma para una app de gestión de tareas con usuarios, proyectos y tareas. Los usuarios pertenecen a proyectos a través de una tabla de membresía con roles (admin, member). Las tareas tienen estado (todo, in_progress, done), prioridad (low, medium, high) y fechas de vencimiento opcionales."

Agregar características: "Agrega un modelo de comentarios al schema. Los usuarios pueden comentar en tareas. Los comentarios tienen contenido y un timestamp de creación. Incluye la relación tanto con User como con Task."

Optimización de consultas: "Agrega paginación a la consulta de tareas. Acepta parámetros page y limit, retorna las tareas junto con el conteo total y el total de páginas."

Migración: "Crea una migración para agregar un campo status a la tabla de posts con valores 'draft', 'published' y 'archived'. Por defecto 'draft'."

Consultas complejas: "Escribe una consulta que retorne los top 10 usuarios por cantidad de publicaciones, incluyendo su conteo total de publicaciones y la fecha de su publicación más reciente."

Cada uno de estos prompts le da a la IA suficiente contexto para generar código correcto y listo para producción. Sé específico sobre tipos de campos, relaciones y restricciones.

Qué sigue

Ahora sabes cómo elegir, configurar y trabajar con bases de datos. Puedes diseñar schemas, ejecutar migraciones, sembrar datos y escribir consultas — todo describiendo lo que quieres a la IA y entendiendo lo que genera.

Pero todos estos datos necesitan protección. En la siguiente lección, implementaremos autenticación y autorización — asegurándonos de que los usuarios son quienes dicen ser y solo pueden acceder a lo que se les permite. Configurarás login con OAuth, protegerás rutas e implementarás control de acceso basado en roles.