Skip to main content
Database Design·Lesson 5 of 5

ORM with Prisma

Prisma is a modern ORM (Object-Relational Mapper) for Node.js and TypeScript. It replaces raw SQL queries with a type-safe, auto-completed API that catches errors at build time instead of runtime.

Why Prisma?

FeatureRaw SQLPrisma
Type safetyNoneFull TypeScript support
Auto-completeNoneSchema-aware IDE completions
MigrationsManual SQL filesAuto-generated from schema
Query buildingString concatenationFluent JavaScript API
Database supportOne at a timePostgreSQL, MySQL, SQLite, etc.

Setting Up Prisma

npm install prisma @prisma/client
npx prisma init

This creates a prisma/ directory with a schema.prisma file and a .env file for your database URL.

Configure the Database

# .env
DATABASE_URL="file:./dev.db"

For PostgreSQL, the URL would look like:

DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

The Prisma Schema

The schema file defines your data models, relationships, and database connection:

// prisma/schema.prisma

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String
  role      String    @default("user")
  posts     Post[]
  comments  Comment[]
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  slug        String    @unique
  content     String
  published   Boolean   @default(false)
  author      User      @relation(fields: [authorId], references: [id])
  authorId    Int
  tags        Tag[]
  comments    Comment[]
  publishedAt DateTime?
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  body      String
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  post      Post     @relation(fields: [postId], references: [id], onDelete: Cascade)
  postId    Int
  createdAt DateTime @default(now())
}

Running Migrations

After defining or modifying your schema, create and run a migration:

# Create a migration
npx prisma migrate dev --name init

# Apply migrations in production
npx prisma migrate deploy

# Reset database (development only)
npx prisma migrate reset

Each migration is stored as a SQL file in prisma/migrations/, giving you a full history of schema changes.

Generating the Client

Prisma generates a type-safe client based on your schema:

npx prisma generate

This runs automatically after prisma migrate dev, but you can run it manually when needed.

CRUD Operations

Create

const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

// Create a user
const user = await prisma.user.create({
  data: {
    name: "Alice",
    email: "alice@example.com",
  },
});

// Create with a relationship
const post = await prisma.post.create({
  data: {
    title: "Getting Started with Prisma",
    slug: "getting-started-prisma",
    content: "Prisma makes database access easy...",
    author: { connect: { id: user.id } },
    tags: {
      connectOrCreate: [
        { where: { name: "prisma" }, create: { name: "prisma" } },
        { where: { name: "database" }, create: { name: "database" } },
      ],
    },
  },
  include: { author: true, tags: true },
});

Read

// Find many
const users = await prisma.user.findMany({
  where: { role: "user" },
  orderBy: { createdAt: "desc" },
  take: 10,
  skip: 0,
});

// Find one by unique field
const user = await prisma.user.findUnique({
  where: { email: "alice@example.com" },
});

// Find first matching
const admin = await prisma.user.findFirst({
  where: { role: "admin" },
});

// Include relations
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
      orderBy: { publishedAt: "desc" },
      include: { tags: true },
    },
  },
});

// Select specific fields
const names = await prisma.user.findMany({
  select: { name: true, email: true },
});

Filtering

// Complex where conditions
const posts = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      {
        OR: [
          { title: { contains: "prisma" } },
          { tags: { some: { name: "database" } } },
        ],
      },
    ],
  },
});

// Comparison operators
const recentUsers = await prisma.user.findMany({
  where: {
    createdAt: { gte: new Date("2026-01-01") },
  },
});

Update

// Update one
const updated = await prisma.user.update({
  where: { id: 1 },
  data: { name: "Alice Updated" },
});

// Update many
await prisma.user.updateMany({
  where: { role: "user" },
  data: { role: "member" },
});

// Upsert (create if not exists, update if exists)
const user = await prisma.user.upsert({
  where: { email: "bob@example.com" },
  update: { name: "Bob Updated" },
  create: { name: "Bob", email: "bob@example.com" },
});

Delete

await prisma.user.delete({ where: { id: 1 } });
await prisma.user.deleteMany({ where: { role: "inactive" } });

Transactions

Run multiple operations atomically:

const [user, post] = await prisma.$transaction([
  prisma.user.create({
    data: { name: "Charlie", email: "charlie@example.com" },
  }),
  prisma.post.create({
    data: {
      title: "My First Post",
      slug: "my-first-post",
      content: "Hello world!",
      authorId: 1,
    },
  }),
]);

// Interactive transaction
await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUnique({ where: { id: 1 } });
  if (!user) throw new Error("User not found");

  await tx.post.updateMany({
    where: { authorId: user.id },
    data: { published: false },
  });
});

Prisma with Express

// src/routes/users.js
const express = require("express");
const router = express.Router();
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();

router.get("/", async (req, res) => {
  const { page = 1, limit = 20, search } = req.query;
  const skip = (parseInt(page) - 1) * parseInt(limit);

  const where = search
    ? {
        OR: [
          { name: { contains: search } },
          { email: { contains: search } },
        ],
      }
    : {};

  const [users, total] = await Promise.all([
    prisma.user.findMany({
      where,
      take: parseInt(limit),
      skip,
      orderBy: { createdAt: "desc" },
      select: { id: true, name: true, email: true, role: true },
    }),
    prisma.user.count({ where }),
  ]);

  res.json({
    users,
    pagination: {
      page: parseInt(page),
      limit: parseInt(limit),
      total,
      pages: Math.ceil(total / parseInt(limit)),
    },
  });
});

module.exports = router;

Prisma Studio

Prisma includes a visual database browser:

npx prisma studio

This opens a web interface at http://localhost:5555 where you can browse, filter, and edit your data.

Practical Exercise

Build a task management API with Prisma:

model Project {
  id          Int      @id @default(autoincrement())
  name        String
  description String?
  tasks       Task[]
  createdAt   DateTime @default(now())
}

model Task {
  id        Int      @id @default(autoincrement())
  title     String
  status    String   @default("todo")
  priority  Int      @default(0)
  project   Project  @relation(fields: [projectId], references: [id], onDelete: Cascade)
  projectId Int
  createdAt DateTime @default(now())
  dueDate   DateTime?
}
// Get project with task statistics
const project = await prisma.project.findUnique({
  where: { id: 1 },
  include: {
    tasks: {
      orderBy: [{ priority: "desc" }, { createdAt: "asc" }],
    },
  },
});

const stats = {
  total: project.tasks.length,
  done: project.tasks.filter((t) => t.status === "done").length,
  inProgress: project.tasks.filter((t) => t.status === "in-progress").length,
  todo: project.tasks.filter((t) => t.status === "todo").length,
};

Key Takeaways

  • Prisma generates a type-safe client from your schema, catching errors before runtime.
  • The schema file defines models, fields, relationships, and constraints in a declarative format.
  • Migrations track schema changes over time and can be applied to any environment.
  • Use include to load relations and select to pick specific fields.
  • Transactions ensure multiple operations succeed or fail together.