Prisma: Type-Safe ORM
Prisma is a modern ORM for Node.js and TypeScript that turns a declarative schema file into a fully typed query client. Instead of hand-writing SQL or wiring up model classes, you describe your data once, run a generator, and get an API where every table, column, and relation is known to the compiler. This page covers the Prisma schema, prisma generate, the type-safe client for CRUD and relations, migrations with prisma migrate, and why the generated types make refactoring safe.
Installing and initializing
Prisma works on any maintained Node.js release; Node 20 or 22 LTS is the sensible default. Install the CLI as a dev dependency and the client as a runtime dependency, then scaffold the project.
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql
prisma init creates a prisma/schema.prisma file and a .env with a DATABASE_URL placeholder. Prisma supports PostgreSQL, MySQL, SQLite, SQL Server, MongoDB, and CockroachDB — swap the provider to match your database.
The schema file
The schema is the single source of truth. It declares three things: the generator (what client to emit), the datasource (which database and connection URL), and your model definitions. Each model maps to a table; each field maps to a column with a type, optional attributes, and relations.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
The ? marks a nullable field, @unique adds a constraint, and the Post[] / User pair declares a one-to-many relation linked by authorId. Connection strings stay in .env, read through env(), so secrets never live in source.
Keep the schema as the canonical definition of your data. Edit it, then regenerate and migrate — never mutate the database by hand, or your schema and reality will drift apart.
Generating the client
Running prisma generate reads the schema and writes a tailored client into node_modules/@prisma/client. The output is specific to your models, so the types you import reflect exactly the tables and fields you defined.
npx prisma generate
Instantiate the client once and reuse it across your app. In a long-running server, a single PrismaClient manages its own connection pool.
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
Create exactly one
PrismaClientinstance per process. Constructing a new client per request exhausts database connections fast. With hot-reloading dev servers, cache the instance onglobalThisto survive reloads.
Type-safe CRUD
Every model exposes the same method surface — create, findUnique, findMany, update, delete, and more. Arguments and return values are fully typed: misnaming a field or selecting a column that does not exist is a compile error, not a runtime surprise.
// Create
const user = await prisma.user.create({
data: { email: "[email protected]", name: "Ada" },
});
// Read one by a unique field
const found = await prisma.user.findUnique({
where: { email: "[email protected]" },
});
// Read many with filtering, ordering, and paging
const recent = await prisma.user.findMany({
where: { name: { not: null } },
orderBy: { createdAt: "desc" },
take: 10,
});
// Update
await prisma.user.update({
where: { id: user.id },
data: { name: "Ada Lovelace" },
});
// Delete
await prisma.user.delete({ where: { id: user.id } });
console.log(found.email);
Output:
[email protected]
Relations and nested writes
Because relations are declared in the schema, Prisma can read and write related rows in one call. Use include to load relations, select to narrow the returned shape, and nested create to insert a parent and its children atomically.
// Create a user together with two posts in a single statement
const author = await prisma.user.create({
data: {
email: "[email protected]",
name: "Grace",
posts: {
create: [
{ title: "Hello Prisma" },
{ title: "Type Safety", published: true },
],
},
},
include: { posts: true },
});
console.log(author.posts.length);
// Query across the relation: users who have a published post
const withPublished = await prisma.user.findMany({
where: { posts: { some: { published: true } } },
select: { email: true, _count: { select: { posts: true } } },
});
console.log(withPublished[0]._count.posts);
Output:
2
2
The returned author is typed with a posts array because of the include, while select returns objects shaped exactly as requested — the compiler tracks both.
Migrations with prisma migrate
When the schema changes, prisma migrate dev diffs it against the database, generates a SQL migration file under prisma/migrations/, applies it, and regenerates the client. The migration history is version-controlled SQL you can review and replay.
# During development: create and apply a migration
npx prisma migrate dev --name add_published_flag
# In CI/production: apply committed migrations without prompting
npx prisma migrate deploy
For quick local experiments against a throwaway database, prisma db push syncs the schema without writing migration files. For inspecting or editing data, npx prisma studio opens a browser-based table viewer.
| Command | Purpose | Where to use |
|---|---|---|
migrate dev | Create + apply a migration, regenerate client | Development |
migrate deploy | Apply existing migrations only | CI / production |
db push | Sync schema, no migration file | Prototyping |
generate | Regenerate the client from the schema | After any schema edit |
studio | Visual data browser | Local inspection |
Why the type safety matters
Prisma’s client types are derived from the schema rather than written by hand, so they cannot drift out of sync with your models. Rename a column in the schema, regenerate, and every stale query lights up red in your editor before the code ever runs. Autocomplete knows the valid fields, filters, and relation names, which removes a whole category of typos and undefined bugs. Even in plain JavaScript projects, editors surface this information through the generated declaration files, so the safety net is not exclusive to TypeScript.
Best Practices
- Treat
schema.prismaas the source of truth; change it, thengenerateandmigraterather than altering the database directly. - Instantiate a single
PrismaClientper process and reuse it; cache it onglobalThisin dev to survive hot reloads. - Use
migrate devlocally andmigrate deployin CI/production, and commit theprisma/migrationsfolder. - Reach for nested
create/include/selectinstead of issuing several round-trips for related data. - Keep
DATABASE_URLand other secrets in environment variables, never in the schema or source. - Run
prisma generatein your build step (e.g. apostinstallscript) so deployments always ship a fresh client. - Wrap multi-step writes that must succeed together in
prisma.$transactionto keep your data consistent.