Next.js + Prisma + PostgreSQL
Configuración full-stack de Next.js 15 con Prisma ORM, base de datos PostgreSQL y Server Actions para mutaciones.
Receta
- Scaffolding de una aplicación Next.js 15 con Tailwind y TypeScript:
npx create-next-app@latest my-app --typescript --tailwind --app cd my-app - Instala Prisma y el cliente:
npm install prisma @prisma/client npm install -D prisma - Inicializa Prisma con PostgreSQL:
npx prisma init --datasource-provider postgresql - Define tu esquema en
prisma/schema.prisma. - Establece
DATABASE_URLen.env. - Ejecuta tu primera migración:
npx prisma migrate dev --name init - Crea un singleton de cliente Prisma en
lib/prisma.tspara evitar agotar conexiones durante la recarga en caliente.
Ejemplo Funcionando
Una aplicación Todo completa con Prisma + PostgreSQL + Server Actions.
prisma/schema.prisma
generator client \{
provider = "prisma-client-js"
\}
datasource db \{
provider = "postgresql"
url = env("DATABASE_URL")
\}
model Todo \{
id String @id @default(cuid())
title String
completed Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
\}lib/prisma.ts (singleton)
import \{ PrismaClient \} from "@prisma/client";
const globalForPrisma = globalThis as unknown as \{
prisma: PrismaClient | undefined;
\};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient(\{
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
\});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;app/page.tsx (Server Component)
import \{ prisma \} from "@/lib/prisma";
import \{ addTodo \} from "./actions";
export default async function Home() \{
const todos = await prisma.todo.findMany(\{
orderBy: \{ createdAt: "desc" \},
\});
return (
<main className="mx-auto max-w-xl p-8">
<h1 className="text-2xl font-bold">Todos</h1>
<form action=\{addTodo\} className="mt-4 flex gap-2">
<input
name="title"
required
className="flex-1 rounded border px-3 py-2"
placeholder="Nuevo todo..."
/>
<button type="submit" className="rounded bg-black px-4 py-2 text-white">
Agregar
</button>
</form>
<ul className="mt-6 space-y-2">
\{todos.map((todo) => (
<li key=\{todo.id\} className="rounded border p-3">
\{todo.title\}
</li>
))\}
</ul>
</main>
);
\}app/actions.ts (Server Action)
"use server";
import \{ prisma \} from "@/lib/prisma";
import \{ revalidatePath \} from "next/cache";
export async function addTodo(formData: FormData) \{
const title = formData.get("title");
if (typeof title !== "string" || title.trim() === "") return;
await prisma.todo.create(\{
data: \{ title: title.trim() \},
\});
revalidatePath("/");
\}Análisis Profundo
Cómo Funciona
Prisma genera un cliente completamente tipificado basado en tu esquema. El Server Component de Next.js se ejecuta en el servidor y consulta Postgres directamente a través del cliente Prisma. Las Server Actions son puntos finales POST creados automáticamente por Next.js — el envío de formularios invoca la acción, que muta la base de datos y llama a revalidatePath para purgar el render almacenado en caché para que los nuevos datos aparezcan en la siguiente solicitud.
El patrón singleton es crítico en desarrollo. La recarga en caliente de Next.js re-evalúa módulos en cada cambio, y sin un singleton crearías un nuevo PrismaClient por recarga, agotando rápidamente el conjunto de conexiones de la base de datos.
Variaciones
- PostgreSQL alojado: Supabase, Neon, Railway o Vercel Postgres funcionan todos — simplemente pega su cadena de conexión en
DATABASE_URL. - Desarrollo local con SQLite: cambia
provider = "sqlite"y usaurl = "file:./dev.db"para una base de datos local sin configuración. - Seeding: agrega un script
prisma/seed.tsy regístralo bajo"prisma": \{ "seed": "tsx prisma/seed.ts" \}enpackage.json, luego ejecutanpx prisma db seed. - Prisma Studio: ejecuta
npx prisma studiopara un navegador de datos y editor basado en navegador. - Prisma Accelerate: agrupamiento de conexiones + almacenamiento en caché global a través de una URL única
prisma://, ideal para despliegues serverless. - ORM Alternativa: Drizzle ORM ofrece SQL tipificado y un runtime más ligero.
Notas TypeScript
- Prisma genera automáticamente tipos para cada modelo. Importa con
import \{ Todo \} from "@prisma/client". - Para resultados de consultas con relaciones, usa
Prisma.TodoGetPayload<\{ include: \{ author: true \} \}>para derivar un tipo exacto de la forma de retorno. - Las Server Actions se pueden tipificar como
(formData: FormData) => Promise<void>o usar el patrón más recienteuseActionStatecon un argumentoprevState. - Habilita modo estricto en
tsconfig.jsonpara detectar campos anulables — Prisma respeta?en esquema comoT | nullen TS.
Trampas
- Singleton o muere. Sin el singleton
globalThisenlib/prisma.ts, el modo dev de Next.js filtrará conexiones hasta que Postgres rechace las nuevas. DATABASE_URLfaltante. Prisma no generará sin ella. Colócala en.env(no solo.env.local) porque la CLI de Prisma lee.env.- Olvida
prisma generate. Después de cambiarschema.prisma, regenera el cliente o tus tipos se desviarán.prisma migrate devlo hace automáticamente, peroprisma db pushy ediciones manuales de esquema pueden no hacerlo. - Migraciones en producción. Usa
prisma migrate deployen CI/CD, nuncaprisma migrate dev— esto último puede solicitar interactivamente y eliminar datos. - Objetivos binarios para despliegue. En plataformas como Vercel o AWS Lambda, es posible que debas agregar
binaryTargets = ["native", "rhel-openssl-3.0.x"]al bloque generador, o tu despliegue fallará con "Binario de motor de consulta no encontrado". - Límites de conexión en serverless. Cada invocación de Lambda/Edge puede abrir una conexión nueva. Usa PgBouncer, Prisma Accelerate o una cadena de conexión agrupada.
- Incompatibilidad de tiempo de ejecución Edge. El cliente Prisma estándar no se ejecuta en Edge. Usa Prisma Accelerate o la versión preliminar de Driver Adapters para soporte de Edge.
Alternativas
| Herramienta | Estilo | Mejor Para |
|---|---|---|
| Prisma | ORM con DSL de esquema | ORM completo tipificado, migraciones, Studio |
| Drizzle ORM | Constructor SQL tipificado | Ligero, amigable con Edge, más cercano a SQL |
| Kysely | Constructor de consultas tipificado | Equipos que quieren SQL-first con inferencia TS |
| postgres.js | Controlador SQL sin procesar | Control máximo, sin sobrecarga de abstracción |
| Cliente Supabase | SDK REST/Realtime | Auth + DB + realtime en un paquete |
Preguntas Frecuentes
¿Por qué necesito un singleton Prisma en Next.js?
El modo dev de Next.js recarga módulos en cambios de archivos. Cada recarga crea una nueva instancia de PrismaClient si ingenuamente haces new PrismaClient() en alcance de módulo, y cada instancia abre su propio conjunto de conexiones. En minutos alcanzas el límite de conexión de tu base de datos. El singleton globalThis sobrevive a la recarga en caliente porque vive en el objeto global, no en el módulo.
¿Cuál es la diferencia entre prisma migrate dev y prisma db push?
migrate dev genera archivos de migración SQL bajo prisma/migrations/ que confirmas en control de versiones — es el flujo seguro para producción. db push sincroniza el esquema directamente sin crear archivos de migración — úsalo solo para prototipado rápido.
¿Puedo usar Prisma con tiempo de ejecución Edge?
No con el cliente estándar. Necesitas Prisma Accelerate (@prisma/extension-accelerate) o los Driver Adapters (actualmente en versión preliminar) que enrutan consultas a través de un proxy basado en fetch en lugar de TCP de Node.
¿Cómo ejecuto migraciones en producción?
Ejecuta npx prisma migrate deploy como parte de tu paso de compilación o lanzamiento. Esto aplica cualquier migración pendiente de prisma/migrations/ sin solicitar y sin generar nuevas.
¿Cómo semilla la base de datos?
Crea prisma/seed.ts, agrega "prisma": \{ "seed": "tsx prisma/seed.ts" \} a package.json, luego ejecuta npx prisma db seed. También se ejecuta automáticamente después de prisma migrate reset.
Trampa: mi despliegue de Vercel falla con "Binario de motor de consulta no encontrado"
Agrega los binaryTargets correctos a schema.prisma:
generator client \{
provider = "prisma-client-js"
binaryTargets = ["native", "rhel-openssl-3.0.x"]
\}Luego confirma, redespliega y asegúrate de que prisma generate se ejecute en tu compilación ("postinstall": "prisma generate" en package.json es la solución estándar).
Trampa: mis tipos no se actualizan después de cambiar el esquema
Ejecuta npx prisma generate manualmente. Es posible que el servidor TS de tu editor también necesite reiniciarse. Normalmente prisma migrate dev ejecuta generate para ti, pero db push y ediciones directas de esquema sin migración no.
TypeScript: ¿Cómo escribo un resultado de consulta que incluye relaciones?
Usa Prisma.<Model>GetPayload:
import \{ Prisma \} from "@prisma/client";
type TodoWithAuthor = Prisma.TodoGetPayload<\{
include: \{ author: true \};
\}>;Esto te da un tipo preciso que coincide exactamente con la forma que devuelve findMany(\{ include: \{ author: true \} \}).
TypeScript: ¿Cómo escribo una Server Action que toma un formulario?
Una acción simple toma FormData y devuelve Promise<void>:
export async function addTodo(formData: FormData): Promise<void> \{ /* ... */ \}Para useActionState agregas un parámetro prevState:
export async function addTodo(prevState: State, formData: FormData): Promise<State> \{ /* ... */ \}¿Debo llamar a Prisma desde Client Components?
No. Prisma solo se ejecuta del lado del servidor. Llámalo desde Server Components, Server Actions, Route Handlers o generateStaticParams. Si un Client Component necesita datos, pásalos como props u obtén a través de una Server Action/Route Handler.
¿Cómo interactúa revalidatePath con mutaciones de Prisma?
revalidatePath("/") le dice a Next.js que descarte el render almacenado en caché para esa ruta. En la siguiente solicitud, el Server Component se vuelve a ejecutar, consulta de nuevo a Prisma y obtiene los datos actualizados. Sin ella, los usuarios ven datos obsoletos hasta que expira el caché.
¿Debería usar Prisma o Drizzle?
Prisma tiene mejor DX, Studio y migraciones. Drizzle es más ligero, nativo de Edge y te da sintaxis similar a SQL con tipos fuertes. Elige Prisma para la mayoría de aplicaciones full-stack; elige Drizzle si te importa el tamaño de paquete, compatibilidad de Edge o quieres mantenerte cerca de SQL.