Skip to content
Node.js nd libraries 5 min read

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 PrismaClient instance per process. Constructing a new client per request exhausts database connections fast. With hot-reloading dev servers, cache the instance on globalThis to 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.

CommandPurposeWhere to use
migrate devCreate + apply a migration, regenerate clientDevelopment
migrate deployApply existing migrations onlyCI / production
db pushSync schema, no migration filePrototyping
generateRegenerate the client from the schemaAfter any schema edit
studioVisual data browserLocal 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.prisma as the source of truth; change it, then generate and migrate rather than altering the database directly.
  • Instantiate a single PrismaClient per process and reuse it; cache it on globalThis in dev to survive hot reloads.
  • Use migrate dev locally and migrate deploy in CI/production, and commit the prisma/migrations folder.
  • Reach for nested create/include/select instead of issuing several round-trips for related data.
  • Keep DATABASE_URL and other secrets in environment variables, never in the schema or source.
  • Run prisma generate in your build step (e.g. a postinstall script) so deployments always ship a fresh client.
  • Wrap multi-step writes that must succeed together in prisma.$transaction to keep your data consistent.
Last updated June 14, 2026
Was this helpful?