Skip to content
Express.js ex data 5 min read

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.

CommandWhen to use it
prisma migrate devLocal development — create and apply a new migration
prisma migrate deployCI/production — apply existing migrations only
prisma generateRegenerate the typed client after a schema change
prisma studioOpen a GUI to browse and edit data
prisma db pushPrototyping — sync schema without a migration file

Tip: prisma migrate dev runs prisma generate for you. Run prisma generate manually after npm install (e.g. in a postinstall script) 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 PrismaClient and import it everywhere; never call new PrismaClient() inside a route handler.
  • Commit your prisma/migrations/ directory and apply changes in production with prisma migrate deploy, never migrate dev.
  • Add prisma generate to a postinstall script so the typed client is regenerated on every fresh install and deploy.
  • Use select to return only the columns you need, keeping payloads small and avoiding accidental exposure of sensitive fields.
  • Fetch relations with include rather than issuing separate queries, and reach for $transaction when several writes must be atomic.
  • On Express 4 wrap async handlers in try/catch and call next(err); on Express 5 rejected promises forward to the error handler automatically.
  • Call prisma.$disconnect() on SIGTERM so the connection pool drains cleanly during graceful shutdown.
Last updated June 14, 2026
Was this helpful?