TypeScript ORM

Prisma

Prisma es un ORM schema-first para TypeScript. Describe tus modelos en schema.prisma, genera un cliente totalmente tipado y deja que las migraciones mantengan la base de datos sincronizada.

intermediate15 min readUpdated 16 sept 2026
prisma/schema.prisma
prisma
// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

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

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  published Boolean  @default(false)
  authorId  String
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())

  @@index([authorId])
}
Lanzado
2019
Última versión mayor
6.x
Escrito en
Cliente en TypeScript, motor opcional en Rust
Archivo de esquema
schema.prisma
Bases de datos
PostgreSQL, MySQL, SQLite, SQL Server, MongoDB
Herramienta de migración
Prisma Migrate

Por que importa

Qué hace que Prisma sea diferente

Tipos derivados del esquema

prisma generate emite un cliente cuyos métodos y tipos de retorno coinciden con tus modelos, por lo que un campo mal escrito se convierte en un error de compilación.

Un único esquema declarativo

El datasource, el generador y cada modelo viven en un único archivo legible que puede ser consumido tanto por personas como por herramientas.

Migraciones en control de versiones

Prisma Migrate convierte los cambios del esquema en archivos SQL revisables que haces commit junto con el código que los necesita.

La imagen completa

Tres ideas que definen a Prisma

Un esquema declarativo, un cliente generado y migraciones que viven en el control de versiones.

El esquema

Declarar

schema.prisma describe el datasource, el generador y cada modelo con sus campos, atributos y relaciones.

El cliente generado

Tipar

prisma generate produce PrismaClient, una API tipada donde cada modelo se convierte en una propiedad y cada consulta devuelve una estructura conocida.

La base de datos

Almacenar

Prisma traduce las llamadas del cliente a SQL y aplica las migraciones, pero la base de datos sigue siendo la fuente de verdad para los datos.

HTML5 de un vistazo

Las piezas que realmente usarás

Cliente generado

Importa PrismaClient y consulta modelos como métodos tipados.

Relaciones e include

Carga registros relacionados con include, o elige campos específicos con select.

Prisma Migrate

migrate dev escribe SQL; migrate deploy lo aplica en producción.

Transacciones

Arrays de $transaction secuenciales o callbacks interactivos.

Prisma CLI

generate, migrate, db push, db seed y studio.

Pooling y Accelerate

Ajusta el connection_limit o coloca un pooler gestionado delante.

Modelo de datos

Qué crea la primera migración

Los modelos User y Post se convierten en dos tablas de PostgreSQL. Prisma añade una clave foránea para la relación y un índice único para el campo email.

Qué crea la primera migraciónTablas de PostgreSQL
  • User.idtextClave primaria UUID generada por @default(uuid())
  • User.emailtextNOT NULL con un índice único de @unique
  • User.nametextNulable, porque el campo está declarado como String?
  • User.createdAttimestamptzNOT NULL, con valor por defecto now() de @default(now())
  • Post.idserialClave primaria entera auto-incremental
  • Post.authorIdtextClave foránea a User.id con ON DELETE CASCADE

Los modelos User y Post se convierten en dos tablas de PostgreSQL. Prisma añade una clave foránea para la relación y un índice único para el campo email.

Una breve historia

De un backend GraphQL a un ORM mainstream

  1. 2016

    Graphcool y un backend GraphQL

    El proyecto que se convertiría en Prisma comienza como una capa GraphQL alojada sobre una base de datos.

    16
  2. 2019

    Prisma 1

    El primer lanzamiento como ORM se sitúa frente a la base de datos y expone una API GraphQL a los clientes.

    19
  3. 2020

    Prisma 2 llega a GA

    Una reescritura hace que el esquema sea la fuente de verdad e introduce el Prisma Client generado y type-safe.

    20
  4. 2021

    Prisma Migrate llega a GA

    Las migraciones declarativas y el seeding se convierten en partes listas para producción del toolkit.

    21
  5. 2024

    Un cliente sin Rust

    El generador prisma-client y los driver adapters mueven más partes del stack hacia TypeScript.

    24

La guia completa

Prisma: Todo lo que necesitas saber

¿Qué es Prisma?

Prisma es un ORM schema-first para TypeScript y Node.js. Describes tus datos una sola vez en schema.prisma, ejecutas un generador y obtienes un cliente cuyos métodos y tipos de retorno provienen directamente de ese esquema. No hay decoradores, ni clases de repositorio, ni cadenas de SQL escritas a mano para los casos comunes.

La propuesta es que el esquema de la base de datos se convierte en un único archivo declarativo que los humanos leen y las herramientas consumen. A partir de él, Prisma produce un cliente tipado que importas en tu código, migraciones SQL que puedes revisar y hacer commit, y una interfaz de estudio (studio UI) para explorar los datos. Prisma es compatible con PostgreSQL, MySQL, MariaDB, SQLite, SQL Server, CockroachDB y MongoDB; el cliente generado traduce cada llamada a SQL o al protocolo de MongoDB antes de mapear las filas nuevamente a objetos simples.

Si has utilizado anteriormente un ORM estilo ActiveRecord, el cambio de mentalidad es que Prisma es schema-first y client-generated, en lugar de ser class-first y runtime-reflective. Esa única decisión explica la mayoría de sus fortalezas y la mayoría de sus costos.

El esquema es la fuente de verdad

Todo comienza en schema.prisma. Contiene tres tipos de bloques: un datasource (donde reside la base de datos), un generator (qué emitir) y un model por cada tabla.

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

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

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

Vale la pena interiorizar algunas convenciones. Un campo que termina en ? es nullable, mientras que un tipo de lista como Post[] es una relación en lugar de una columna. Los atributos comienzan con @ para los campos y @@ para los bloques. @id marca la clave primaria, @unique crea un índice único, @default(...) proporciona un valor, y @map y @@map renombran la columna o tabla subyacente cuando no coincide con tu modelo.

El modelo anterior se mapea a una tabla User con las columnas id, email, name y createdAt, además de una relación virtual posts que Prisma resuelve mediante un join o una segunda consulta.

Generando y utilizando el cliente

El cliente de Prisma es código generado. Después de editar el esquema, ejecuta:

pnpm prisma generate

Esto lee schema.prisma y escribe un cliente en node_modules/.prisma/client, o en una carpeta que elijas utilizando el generador más reciente prisma-client. Luego, lo instancias una vez y lo reutilizas:

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const user = await prisma.user.findUnique({
  where: { email: "[email protected]" },
});

Debido a que el cliente es generado, tanto prisma.user como la estructura de user son conocidos por el comprobador de tipos. Si renombras un campo en el esquema y regeneras el cliente, cualquier uso desactualizado fallará al compilar. Ese ciclo de retroalimentación es la razón principal por la cual los equipos adoptan Prisma.

En entornos serverless o con hot-reloading, evita crear un nuevo cliente por cada solicitud o recarga. En su lugar, adjunta uno al objeto global:

import { PrismaClient } from "@prisma/client";

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? new PrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;

Lectura de datos

Prisma expone un método por cada forma de lectura. findUnique obtiene una única fila mediante un campo único, findFirst obtiene una fila que coincida con un filtro arbitrario y findMany devuelve una lista.

const post = await prisma.post.findUnique({ where: { id: 42 } });

const latest = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: "desc" },
});

const posts = await prisma.post.findMany({
  where: {
    published: true,
    title: { contains: "prisma", mode: "insensitive" },
    authorId: { in: [userId] },
  },
  orderBy: { createdAt: "desc" },
  take: 20,
  skip: 0,
});

Los operadores de filtrado utilizan nombres en lugar de símbolos: equals, not, in, notIn, lt, lte, gt, gte, contains, startsWith, endsWith y mode. Combínalos con arrays AND, OR y NOT:

const posts = await prisma.post.findMany({
  where: {
    OR: [
      { title: { contains: "orm" } },
      { author: { email: { endsWith: "@example.com" } } },
    ],
    NOT: { published: false },
  },
});

take y skip implementan la paginación por desplazamiento (offset pagination). Para conjuntos de resultados grandes, es preferible usar la paginación por cursor (cursor pagination), que pagina desde la última fila en lugar de contar desde el principio:

const page = await prisma.post.findMany({
  take: 20,
  skip: 1,
  cursor: { id: lastSeenId },
  orderBy: { id: "asc" },
});

findUnique no aceptará un where que no sea único; para esos casos, utiliza findFirst. Las variantes findUniqueOrThrow y findFirstOrThrow lanzan un error en lugar de devolver null, lo que permite eliminar una ramificación condicional cuando la fila debe existir obligatoriamente.

Escritura de datos

Las operaciones de creación, actualización y eliminación están tipadas de la misma manera. create, update, upsert y delete operan sobre una sola fila, mientras que createMany, updateMany y deleteMany operan sobre conjuntos.

const user = await prisma.user.create({
  data: { email: "[email protected]", name: "Ada" },
});

await prisma.user.update({
  where: { id: user.id },
  data: { name: "Ada Lovelace" },
});

await prisma.user.upsert({
  where: { email: "[email protected]" },
  update: { name: "Ada Lovelace" },
  create: { email: "[email protected]", name: "Ada" },
});

await prisma.user.delete({ where: { id: user.id } });

Para inserciones masivas, createMany emite un único INSERT y es drásticamente más rápido que iterar sobre create:

await prisma.post.createMany({
  data: [
    { title: "Hello", authorId: user.id },
    { title: "World", authorId: user.id },
  ],
  skipDuplicates: true,
});

La desventaja es que createMany no puede escribir relaciones anidadas; está diseñado únicamente para filas planas. updateMany y deleteMany aceptan los mismos filtros que findMany, por lo que la ausencia de un where realmente afecta a todas las filas.

Relaciones, include y select

Las relaciones se declaran en ambos lados. User.posts es una lista y Post.author es un valor único, vinculados mediante @relation(fields: [authorId], references: [id]) en el lado propietario.

Por defecto, Prisma devuelve únicamente las columnas escalares. Para cargar una relación, debes añadir include:

const user = await prisma.user.findUnique({
  where: { id: userId },
  include: {
    posts: {
      where: { published: true },
      orderBy: { createdAt: "desc" },
      take: 10,
    },
  },
});

select es la herramienta más precisa: elige exactamente qué campos devolver, tanto para el modelo como para las relaciones anidadas.

const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    posts: {
      select: { title: true },
      where: { published: true },
    },
    _count: { select: { posts: true } },
  },
});

No puedes combinar select y include en el mismo nivel, ya que select ya responde a la pregunta de qué devolver. Utiliza select cuando un endpoint tenga una estructura de respuesta fija, y include cuando realmente necesites el registro relacionado completo. Devolver filas enteras para luego filtrarlas en JavaScript desperdicia ancho de banda y memoria, por lo que select evita una regresión común.

Escrituras anidadas

Una de las mejores características de Prisma es la capacidad de escribir un padre y sus hijos en una sola llamada. Las operaciones anidadas create, connect, update y delete se ejecutan todas dentro de una transacción implícita.

const user = await prisma.user.create({
  data: {
    email: "[email protected]",
    posts: {
      create: [
        { title: "First post" },
        { title: "Second post", published: true },
      ],
    },
  },
  include: { posts: true },
});

Para conectar filas existentes en lugar de crearlas se utiliza connect, y para desconectar una relación se utiliza disconnect:

await prisma.post.update({
  where: { id: postId },
  data: {
    author: { connect: { email: "[email protected]" } },
  },
});

Las escrituras anidadas mantienen la consistencia de los datos relacionados sin que tengas que abrir una transacción manualmente, que es exactamente el tipo de gestión que un ORM debería manejar.

Migraciones en desarrollo y producción

Prisma Migrate convierte la diferencia entre tu schema y tu base de datos en archivos SQL versionados dentro de prisma/migrations.

pnpm prisma migrate dev --name add_posts
pnpm prisma migrate deploy
pnpm prisma migrate status

migrate dev es un comando de desarrollo. Compara el schema con la base de datos, escribe una nueva migración, la aplica y regenera el client. También utiliza una shadow database para detectar el drift, por lo que el rol de desarrollo necesita permisos para crear y eliminar bases de datos. Si se encuentra un drift, es posible que sugiera resetear la base de datos, lo cual elimina los datos.

migrate deploy es el comando de producción. Aplica las migraciones pendientes y nada más: sin generación, sin shadow database y sin resets. Ejecútalo en tu pipeline de despliegue antes de que el nuevo código comience a recibir tráfico.

Para prototipos desechables, prisma db push omite completamente el historial de migraciones y fuerza a la base de datos a coincidir con el schema:

pnpm prisma db push

db push es rápido y conveniente, pero no deja un rastro de auditoría y puede eliminar columnas o tablas para que la base de datos se ajuste al schema. Nunca lo apuntes a producción. Otros comandos útiles son prisma migrate reset para eliminar, recrear y volver a sembrar (seed) una base de datos de desarrollo, y prisma migrate diff para mostrar qué cambiaría sin llegar a aplicarlo.

Seeding

Los datos de seed deben ir en prisma/seed.ts. Regístralo en package.json para que Prisma sepa cómo ejecutarlo:

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

const prisma = new PrismaClient();

async function main() {
  await prisma.user.upsert({
    where: { email: "[email protected]" },
    update: {},
    create: {
      email: "[email protected]",
      name: "Ada",
      posts: { create: [{ title: "Welcome" }] },
    },
  });
}

main()
  .then(() => prisma.$disconnect())
  .catch(async (error) => {
    console.error(error);
    await prisma.$disconnect();
    process.exit(1);
  });

Ejecútalo con pnpm prisma db seed. Debido a que el seeding se ejecuta después de migrate dev y migrate reset, una base de datos recién creada nunca estará vacía, lo que hace que el onboarding y las pruebas sean predecibles.

Transacciones

Prisma dispone de dos API de transacciones. La forma secuencial recibe un array de operaciones y las ejecuta en orden:

const [debit, credit] = await prisma.$transaction([
  prisma.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 5000 } },
  }),
  prisma.account.update({
    where: { id: 2 },
    data: { balance: { increment: 5000 } },
  }),
]);

La forma interactiva te proporciona un cliente de transacción y te permite tomar decisiones basadas en los resultados:

await prisma.$transaction(async (tx) => {
  const sender = await tx.account.findUniqueOrThrow({ where: { id: 1 } });
  if (sender.balance < 5000) throw new Error("insufficient_funds");

  await tx.account.update({
    where: { id: 1 },
    data: { balance: { decrement: 5000 } },
  });
  await tx.account.update({
    where: { id: 2 },
    data: { balance: { increment: 5000 } },
  });
});

Utiliza tx para cada consulta dentro de una transacción interactiva; si usas el cliente prisma externo, saldrás de la transacción. Puedes ajustar el comportamiento con maxWait, que controla cuánto tiempo esperar por una conexión, y timeout, que limita la duración máxima de la transacción, además de definir el nivel de aislamiento con isolationLevel. Mantén las transacciones cortas y nunca dejes una abierta durante una llamada HTTP o una interacción del usuario.

SQL puro cuando lo necesites

El query builder cubre la mayoría de las lecturas, pero las consultas de reportes y las funcionalidades específicas de la base de datos a veces requieren SQL. $queryRaw es una tagged template que parametriza los valores por ti:

import { Prisma } from "@prisma/client";

const rows = await prisma.$queryRaw<
  { day: Date; revenue: bigint }[]
>(Prisma.sql`
  SELECT date_trunc('day', created_at) AS day,
         sum(total_cents)             AS revenue
  FROM orders
  WHERE status = 'paid'
  GROUP BY 1
  ORDER BY 1 DESC
`);

Usa $queryRaw para SELECT y $executeRaw para escrituras. Ambos aceptan tagged templates, lo que evita la inyección de SQL ya que los valores interpolados se convierten en parámetros vinculados. $queryRawUnsafe y $executeRawUnsafe existen para SQL genuinamente dinámico y deben considerarse peligrosos: nunca interpoles entradas de usuario en ellos. Prisma.sql y Prisma.join te permiten componer fragmentos manteniendo intacta la parametrización.

Pooling, serverless y Accelerate

Cada instancia de PrismaClient posee un pool de conexiones. En un servidor de larga duración, esto es exactamente lo que se busca. En una plataforma serverless, cada instancia de función crea su propio cliente y, por lo tanto, su propio pool; un pico de tráfico puede agotar las max_connections de la base de datos en cuestión de segundos.

La primera herramienta es la cadena de conexión. Limita el pool y el tiempo de espera:

DATABASE_URL="postgresql://user:pass@host:5432/shop?connection_limit=5&pool_timeout=10"

En funciones donde cada instancia maneja una sola solicitud a la vez, connection_limit=1 suele ser lo correcto. Si utilizas PgBouncer, añade ?pgbouncer=true para que Prisma deje de usar prepared statements que el transaction pooling no puede preservar. Si no puedes modificar los límites de conexión de la base de datos, Prisma Accelerate actúa como un pooler gestionado y caché; envuelves el cliente con su extensión y rutas las consultas a través de un endpoint global. El antiguo Data Proxy resolvía el mismo problema y ha sido sustituido por Accelerate.

Sea cual sea tu elección, la regla es la misma: reutiliza un único cliente por proceso y evita que el número de instancias de la aplicación multiplique las conexiones más allá de lo que la base de datos puede soportar.

La cuestión del N+1

Un problema de N+1 ocurre cuando obtienes una lista y luego ejecutas una consulta por cada fila para cargar una relación. Prisma evita la forma clásica porque include y select cargan las relaciones como parte de la misma llamada: para un findMany con una relación, ejecuta una consulta para los padres y otra para los hijos, no una por cada padre.

Tú mismo puedes reintroducir el problema si utilizas un bucle:

const users = await prisma.user.findMany();

for (const user of users) {
  // One query per user: this is the N+1.
  const posts = await prisma.post.findMany({ where: { authorId: user.id } });
}

En su lugar, carga la relación en la consulta original:

const users = await prisma.user.findMany({
  include: { posts: true },
});

Para lecturas profundamente anidadas, la estrategia predeterminada de Prisma ejecuta una consulta por cada nivel de relación. En los casos donde una única consulta con join sea materialmente más rápida, la opción relationLoadStrategy: "join" le indica a Prisma que utilice un LEFT JOIN en su lugar. Mide el rendimiento antes de cambiar, ya que ambas estrategias intercambian la cantidad de viajes de ida y vuelta (round trips) por la duplicación de columnas del padre.

Mejores prácticas

  • Trata a schema.prisma como la fuente de verdad y modifícalo a través de migraciones, nunca manualmente.
  • Instancia PrismaClient una sola vez por proceso y reutilízalo; protege el hot reload con un global.
  • Usa select para formas de respuesta fijas y include solo cuando necesites los registros completos.
  • Prefiere la paginación por cursor sobre offsets grandes de skip.
  • Usa createMany para inserciones masivas y escrituras anidadas para filas relacionadas.
  • Mantén las transacciones interactivas cortas y usa siempre el cliente tx dentro de ellas.
  • Recurre a $queryRaw con tagged templates, nunca a las variantes Unsafe, cuando necesites SQL.
  • Configura connection_limit y, en entornos serverless, utiliza un pooler como Accelerate o PgBouncer.
  • Haz commit de las carpetas de migraciones y ejecuta migrate deploy en CI/CD en lugar de db push.

Errores comunes

  • Ejecutar prisma db push contra producción y perder columnas.
  • Crear un nuevo PrismaClient por cada solicitud y agotar las conexiones.
  • Olvidar prisma generate después de un cambio de esquema y luego preguntarse por qué los tipos están desactualizados.
  • Obtener filas completas con include y filtrar los campos en JavaScript.
  • Usar el cliente externo dentro de una transacción interactiva, lo que provoca que se salga de la transacción silenciosamente.
  • Ignorar where en updateMany o deleteMany y actualizar todas las filas.
  • Asumir que findUnique acepta cualquier filtro; requiere un campo único.
  • Interpolar la entrada del usuario en $queryRawUnsafe.
  • Mantener una transacción abierta mientras se espera la respuesta de una API externa.

Próximos pasos

Prisma es una de las opciones para interactuar con una base de datos relacional desde TypeScript. Compáralo con Drizzle, que mantiene el SQL visible y prescinde del cliente generado, y con TypeORM, el enfoque basado en clases y decoradores que es anterior a ambos. Debajo de cada uno de ellos está la base de datos en sí, por lo que la guía de PostgreSQL es un conocimiento profundo que vale la pena independientemente del ORM. Si aún estás eligiendo un runtime para el servidor que alojará todo esto, comienza con Node.js.

En la practica

Esquema, consulta, escritura anidada, transacción

Las cuatro estructuras que más escribirás en un proyecto de Prisma.

prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

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

model User {
  id        String   @id @default(uuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
}

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  published Boolean  @default(false)
  authorId  String
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  createdAt DateTime @default(now())

  @@index([authorId])
}

Obtener solo lo que necesitas

select elige campos exactos para la respuesta, mientras que include carga filas relacionadas completas que luego podrías descartar.

Preferir
const emails = await prisma.user.findMany({
  select: { email: true },
});
Evitar
const users = await prisma.user.findMany({
  include: { posts: true },
});

// Most of each row is discarded in JavaScript.
const emails = users.map((u) => u.email);

Migrar frente a prototipar

migrate dev escribe SQL versionado que puedes revisar y desplegar. db push fuerza el esquema sin historial y es para bases de datos desechables.

Preferir
pnpm prisma migrate dev --name add_posts
pnpm prisma migrate deploy
Evitar
# No migration files, no audit trail,
# and columns can be dropped to fit.
pnpm prisma db push

Compromisos

¿Debería Prisma ser tu ORM?

Prisma optimiza la seguridad y la productividad. Ese intercambio vale la pena para la mayoría del código de aplicación, pero menos cuando necesitas un control total.

Strengths

  • Productivo desde el primer modelo

    El esquema, el cliente generado y la herramienta de migración comparten un mismo modelo mental, por lo que hay muy poco código de unión que escribir.

  • Seguridad impuesta por el compilador

    Los tipos se derivan del esquema, lo que detecta campos renombrados, formas de argumentos incorrectas y relaciones faltantes antes del tiempo de ejecución.

  • Migraciones y seeding integrados

    Prisma Migrate, los scripts de seed y Prisma Studio cubren el trabajo diario con la base de datos sin necesidad de librerías extra.

Trade-offs

  • Menos control sobre el SQL

    Cambias parte del control de las consultas por la abstracción. Los reportes complejos a menudo requieren SQL puro, donde la seguridad de tipos es menor.

  • Un cliente generado es un paso de build

    Cambiar el esquema implica regenerar; olvidar hacerlo en CI o en un clon fresco produce errores de tipos confusos.

  • Serverless requiere atención

    Un cliente por instancia de función multiplica las conexiones, por lo que los despliegues serverless necesitan límites de conexión o un pooler como Accelerate.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender Prisma?

Nuestro tutorial interactivo te guia a traves de Prisma paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.