Prisma ORM
Prisma is a modern, type-safe ORM that turns a declarative schema into a fully typed database client. Instead of writing raw SQL or wiring up model classes by hand, you describe your tables and relations in a single schema.prisma file, run a generator, and get an auto-completing PrismaClient whose query results are exactly typed to match your data. For an Express API this means fewer runtime surprises, safe migrations you can commit to version control, and queries that your editor checks before the request ever runs.
Installing Prisma and initializing the schema
Install the Prisma CLI as a dev dependency and the runtime client as a regular dependency, then initialize the project. The --datasource-provider flag scaffolds the schema for your database of choice.
npm install express
npm install --save-dev prisma
npm install @prisma/client
npx prisma init --datasource-provider postgresql
This creates a prisma/ directory containing schema.prisma and adds a DATABASE_URL entry to your .env file. Point that URL at your database, for example postgresql://user:pass@localhost:5432/mydb?schema=public.
The Prisma schema
The schema is the single source of truth. It declares the datasource, the client generator, and your data models. Each model maps to a table; each field maps to a column with a type, optional modifiers (? for nullable, [] for lists), and attributes such as @id, @unique, and @default.
// prisma/schema.prisma
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[] // one-to-many relation
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 posts Post[] field on User and the author/authorId pair on Post together define a one-to-many relation: a user has many posts, and each post belongs to one user. Prisma reads these relation fields to generate type-safe nested queries.
Migrations: from schema to database
When you change the schema you create a migration. prisma migrate dev diffs your schema against the database, generates a timestamped SQL file under prisma/migrations/, applies it, and regenerates the client — all in one command. The SQL is committed to your repository so every environment evolves the same way.
npx prisma migrate dev --name init
Output:
Applying migration `20260614100000_init`
The following migration(s) have been created and applied:
migrations/
└─ 20260614100000_init/
└─ migration.sql
✔ Generated Prisma Client (v6.x) in 84ms
In CI and production you do not generate new migrations; you only apply the committed ones with prisma migrate deploy, which is non-interactive and never modifies the schema.
| Command | When to use it |
|---|---|
prisma migrate dev | Local development — create and apply a new migration |
prisma migrate deploy | CI/production — apply existing migrations only |
prisma generate | Regenerate the typed client after a schema change |
prisma studio | Open a GUI to browse and edit data |
prisma db push | Prototyping — sync schema without a migration file |
Tip:
prisma migrate devrunsprisma generatefor you. Runprisma generatemanually afternpm install(e.g. in apostinstallscript) so the client exists in fresh deployments.
Instantiating PrismaClient
The generated PrismaClient opens a connection pool internally, so — exactly like a pg pool — you create one instance and share it across your whole app. Creating a client per request exhausts database connections.
// db.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
module.exports = prisma;
Wire it into the Express app and disconnect cleanly on shutdown.
// server.js
const express = require('express');
const prisma = require('./db');
const app = express();
app.use(express.json()); // parse JSON request bodies
app.use('/users', require('./routes/users'));
const server = app.listen(3000, () => console.log('Listening on :3000'));
process.on('SIGTERM', async () => {
await prisma.$disconnect();
server.close();
});
Type-safe queries in route handlers
Every model is exposed as a property on the client (prisma.user, prisma.post) with methods like findMany, findUnique, create, update, and delete. The arguments and return types are inferred from your schema, so a typo in a field name is a compile-time error in TypeScript and an autocomplete miss in plain JavaScript.
// routes/users.js
const express = require('express');
const prisma = require('../db');
const router = express.Router();
// CREATE — nested write also inserts related posts
router.post('/', async (req, res, next) => {
try {
const user = await prisma.user.create({
data: {
email: req.body.email,
name: req.body.name,
posts: { create: req.body.posts ?? [] },
},
});
res.status(201).json(user);
} catch (err) {
next(err);
}
});
// READ — include the related posts in one query
router.get('/:id', async (req, res, next) => {
try {
const user = await prisma.user.findUnique({
where: { id: Number(req.params.id) },
include: { posts: true },
});
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
} catch (err) {
next(err);
}
});
module.exports = router;
The include option pulls related records in a single, type-safe call — no manual joins. A read response looks like this:
Output:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
{
"id": 1,
"email": "[email protected]",
"name": "Ada Lovelace",
"createdAt": "2026-06-14T10:00:00.000Z",
"posts": [
{ "id": 7, "title": "Hello", "published": true, "authorId": 1 }
]
}
For multi-step writes that must all succeed or fail together, use prisma.$transaction([...]), which runs an array of queries atomically.
Best Practices
- Create exactly one
PrismaClientand import it everywhere; never callnew PrismaClient()inside a route handler. - Commit your
prisma/migrations/directory and apply changes in production withprisma migrate deploy, nevermigrate dev. - Add
prisma generateto apostinstallscript so the typed client is regenerated on every fresh install and deploy. - Use
selectto return only the columns you need, keeping payloads small and avoiding accidental exposure of sensitive fields. - Fetch relations with
includerather than issuing separate queries, and reach for$transactionwhen several writes must be atomic. - On Express 4 wrap async handlers in
try/catchand callnext(err); on Express 5 rejected promises forward to the error handler automatically. - Call
prisma.$disconnect()onSIGTERMso the connection pool drains cleanly during graceful shutdown.