prisma
Prisma is a database toolkit for Node.js and TypeScript: you describe your tables in a schema.prisma file, the CLI generates a fully typed client for queries, Prisma Migrate turns schema changes into SQL migrations, and Prisma Studio gives you a GUI over the data. The prisma npm package is the CLI; your app imports the generated client. Version 7 dropped the Rust query engine in favor of a TypeScript client that talks to the database through driver adapters.
Still the most complete typed database toolkit for TypeScript, and v7's engine-free client fixed the old binary headaches. Weigh the setup ceremony and the fact that active invention has moved to the Prisma Next rewrite; for SQL-comfortable teams Drizzle or Kysely are lighter and less eventful.
Use it if
- You want end-to-end typed queries where renaming a column breaks the build instead of production
- Your team is stronger in TypeScript than SQL and wants migrations, client and data browser from one tool
- You are on PostgreSQL, MySQL, SQLite, SQL Server or CockroachDB, the databases the schema DSL models best
- You value a declarative schema file as the single source of truth that code review can actually read
- You think in SQL and want control over every query; the client's query object language gets awkward for window functions, CTEs and reporting queries, and you will drop to $queryRaw often enough to question the abstraction
- You need a lean dependency: the toolchain brings a schema DSL, a codegen step on every schema change, a config file and a driver adapter, which is a lot of machinery for a small service
- The project's center of gravity has moved: the repo's main branch is now Prisma Next, an early-access rewrite, while ORM 7 is maintained on the v7 branch; if betting on a rewritten-underneath tool worries you, a plain query builder is the calmer choice
- You are mostly on MongoDB; support exists but the tool is clearly SQL-first
Setup reality
More moving parts than any competing ORM. You need schema.prisma with a generator block plus output path, a prisma.config.ts for the CLI, and a driver adapter package like @prisma/adapter-pg passed to the client constructor. Since v7 .env files are not loaded automatically with prisma.config.ts, so add dotenv/config or node --env-file yourself. Every schema change requires npx prisma generate before types update, which trips up everyone at least once.
Patterns
Define models in schema.prisma (v7 shape)define-schema
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
email String @unique
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
author User @relation(fields: [authorId], references: [id])
authorId Int
}v7 uses provider prisma-client with a required output path; the old prisma-client-js default and node_modules output are gone.
prisma.config.ts for the CLIconfigure-cli
import 'dotenv/config'
import { defineConfig, env } from 'prisma/config'
export default defineConfig({
schema: 'prisma/schema.prisma',
migrations: { path: 'prisma/migrations' },
datasource: { url: env('DATABASE_URL') },
})With prisma.config.ts, .env is NOT auto-loaded anymore; the dotenv/config import at the top is doing real work.
Create the client with a driver adapterinstantiate-client
import { PrismaClient } from './generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
export const prisma = new PrismaClient({ adapter })v7 requires a driver adapter (@prisma/adapter-pg here) and you import from your generated output path, not from @prisma/client directly.
Create and apply a migrationmigrate
npx prisma migrate dev --name add_posts # dev: diff, apply, regenerate
npx prisma migrate deploy # prod: apply pending migrations
npx prisma generate # regenerate client after schema editsmigrate dev is for development only; it can reset data. CI and production should run migrate deploy.
Create and query recordscrud-create-read
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
posts: { create: { title: 'hello' } },
},
})
const users = await prisma.user.findMany({
where: { email: { endsWith: '@example.com' } },
select: { id: true, email: true },
})Nested create writes parent and child in one call; select trims the payload and the return type follows it.
Load relations with includerelations-include
const usersWithPosts = await prisma.user.findMany({
include: {
posts: {
where: { title: { contains: 'prisma' } },
orderBy: { id: 'desc' },
take: 3,
},
},
})include and select are mutually exclusive at the same level; combining them is a common first error.
Update or upsert a recordupdate-upsert
await prisma.post.update({
where: { id: 42 },
data: { title: 'updated' },
})
await prisma.user.upsert({
where: { email: 'alice@example.com' },
update: { email: 'alice@example.com' },
create: { email: 'alice@example.com' },
})update throws if the record does not exist; use updateMany or upsert when absence is a normal case.
Run queries in a transactiontransactions
await prisma.$transaction(async (tx) => {
const from = await tx.account.update({
where: { id: 1 },
data: { balance: { decrement: 100 } },
})
if (from.balance < 0) throw new Error('insufficient funds')
await tx.account.update({
where: { id: 2 },
data: { balance: { increment: 100 } },
})
})Throwing inside the callback rolls everything back; use the tx handle, not the outer prisma, or queries escape the transaction.
Cursor-based paginationpagination
const page = await prisma.post.findMany({
take: 20,
skip: cursor ? 1 : 0,
cursor: cursor ? { id: cursor } : undefined,
orderBy: { id: 'asc' },
})
const nextCursor = page.at(-1)?.idskip: 1 excludes the cursor row itself; offset pagination (skip/take alone) degrades on large tables.
Escape hatch to raw SQLraw-sql
const rows = await prisma.$queryRaw`
SELECT author_id, COUNT(*)::int AS posts
FROM "Post"
GROUP BY author_id
HAVING COUNT(*) > ${min}
`The tagged template parameterizes ${min} safely; $queryRawUnsafe with string concatenation is the injection foot-gun.
Handle known Prisma errorserror-handling
import { Prisma } from './generated/prisma/client'
try {
await prisma.user.create({ data: { email } })
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
return { error: 'email already taken' }
}
throw e
}P2002 is the unique-constraint code; matching on error codes beats parsing messages that change between versions.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| drizzle-orm | npm | You want typed queries that stay close to SQL with no codegen step and a much smaller runtime |
| kysely | npm | You want a pure type-safe SQL query builder and are happy managing migrations separately |
| typeorm | npm | Decorator-and-entity style ORM in an existing NestJS codebase where it is already the convention |