Prisma ORM — Complete TypeScript Guide 2026

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Introduction

Why This Matters

Prisma is the most widely adopted TypeScript ORM in 2026, used by companies ranging from startups to enterprises. Its auto-generated client, schema-first approach, and excellent developer experience make it the fastest way to start building a type-safe database layer. If you are building a full-stack TypeScript application with Next.js, NestJS, or Express, Prisma is likely the smoothest path to a production-ready data layer.

Installation and Initialization

npm install @prisma/client
npm install --save-dev prisma
 
# Initialize Prisma in your project
npx prisma init

This creates prisma/schema.prisma and a .env file with a DATABASE_URL placeholder.

Defining the Schema

Prisma uses its own schema language — the Prisma Schema Language (PSL). All models, relations, and database settings live in prisma/schema.prisma.

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
 
generator client {
  provider = "prisma-client-js"
}
 
enum Role {
  USER
  ADMIN
  MODERATOR
}
 
model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  name      String
  role      Role      @default(USER)
  isActive  Boolean   @default(true)
  createdAt DateTime  @default(now())
  updatedAt DateTime  @updatedAt
  posts     Post[]
  profile   Profile?
 
  @@index([role])
}
 
model Profile {
  id     Int    @id @default(autoincrement())
  bio    String
  userId Int    @unique
  user   User   @relation(fields: [userId], references: [id], onDelete: Cascade)
}
 
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  tags      Tag[]
  createdAt DateTime @default(now())
 
  @@index([authorId])
}
 
model Tag {
  id    Int    @id @default(autoincrement())
  name  String @unique
  posts Post[]
}

After editing the schema, generate or update the Prisma Client:

npx prisma generate

Prisma Client — CRUD Operations

// src/db/client.ts
import { PrismaClient } from '@prisma/client';
 
// Single instance pattern — prevents connection pool exhaustion in dev
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
 
export const prisma =
  globalForPrisma.prisma ??
  new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'warn', 'error']
      : ['warn', 'error'],
  });
 
if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma;
}
// src/db/users.ts
import { prisma } from './client';
import { Prisma, User } from '@prisma/client';
 
// Find by ID with related data
export async function getUserWithPosts(id: number) {
  return prisma.user.findUnique({
    where: { id },
    include: {
      posts: {
        where: { published: true },
        orderBy: { createdAt: 'desc' },
        take: 10,
      },
      profile: true,
    },
  });
}
 
// Create with nested write
export async function createUserWithProfile(
  email: string,
  name: string,
  bio: string
) {
  return prisma.user.create({
    data: {
      email,
      name,
      profile: {
        create: { bio },
      },
    },
    include: { profile: true },
  });
}
 
// Filtered list with pagination
export async function listUsers(
  page: number,
  pageSize: number,
  search?: string
): Promise<{ users: User[]; total: number }> {
  const where: Prisma.UserWhereInput = search
    ? { OR: [{ email: { contains: search } }, { name: { contains: search } }] }
    : {};
 
  const [users, total] = await prisma.$transaction([
    prisma.user.findMany({
      where,
      skip: (page - 1) * pageSize,
      take: pageSize,
      orderBy: { createdAt: 'desc' },
    }),
    prisma.user.count({ where }),
  ]);
 
  return { users, total };
}
 
// Update
export async function updateUserRole(id: number, role: 'USER' | 'ADMIN') {
  return prisma.user.update({
    where: { id },
    data: { role },
  });
}
 
// Upsert — create or update
export async function upsertUser(email: string, name: string) {
  return prisma.user.upsert({
    where: { email },
    create: { email, name },
    update: { name },
  });
}
 
// Soft delete pattern
export async function deactivateUser(id: number) {
  return prisma.user.update({
    where: { id },
    data: { isActive: false },
  });
}

Transactions

Prisma supports two types of transactions: sequential (in $transaction([])) and interactive (with a callback).

// Sequential transaction — runs queries in order, all succeed or all fail
export async function transferPosts(fromUserId: number, toUserId: number) {
  await prisma.$transaction([
    prisma.post.updateMany({
      where: { authorId: fromUserId },
      data: { authorId: toUserId },
    }),
    prisma.user.update({
      where: { id: fromUserId },
      data: { isActive: false },
    }),
  ]);
}
 
// Interactive transaction — full control, can read within the transaction
export async function createPostWithTagsTransaction(
  authorId: number,
  title: string,
  body: string,
  tagNames: string[]
) {
  return prisma.$transaction(async (tx) => {
    const post = await tx.post.create({
      data: { title, body, authorId },
    });
 
    // Upsert tags and connect them
    const tags = await Promise.all(
      tagNames.map((name) =>
        tx.tag.upsert({
          where: { name },
          create: { name },
          update: {},
        })
      )
    );
 
    await tx.post.update({
      where: { id: post.id },
      data: {
        tags: { connect: tags.map((t) => ({ id: t.id })) },
      },
    });
 
    return tx.post.findUnique({
      where: { id: post.id },
      include: { tags: true },
    });
  });
}

Migrations

# Create and apply a migration
npx prisma migrate dev --name add_user_bio
 
# Apply migrations in production (no dev mode)
npx prisma migrate deploy
 
# Check migration status
npx prisma migrate status
 
# Open Prisma Studio (database browser)
npx prisma studio

Raw Queries

Sometimes you need SQL that Prisma cannot express. Use $queryRaw for typed results.

import { Prisma } from '@prisma/client';
 
interface UserStats {
  id: number;
  email: string;
  post_count: bigint;
}
 
export async function getUserStats(): Promise<UserStats[]> {
  return prisma.$queryRaw<UserStats[]>(
    Prisma.sql`
      SELECT u.id, u.email, COUNT(p.id) AS post_count
      FROM users u
      LEFT JOIN posts p ON p.author_id = u.id
      GROUP BY u.id, u.email
      ORDER BY post_count DESC
      LIMIT 10
    `
  );
}

Common Mistakes

Mistake 1 — Creating a new PrismaClient on every request in Next.js: Next.js hot-reloads create new module instances, exhausting database connections. Use the global singleton pattern shown above.

Mistake 2 — Over-using include: deeply nested include can generate massive JOIN queries. Use select to fetch only the columns you need.

Mistake 3 — Ignoring $transaction for multi-step writes: if two prisma.create() calls run independently and the second fails, the first is not rolled back — you get partial data.

Mistake 4 — Using $queryRaw with string interpolation: prisma.$queryRaw(SELECT * FROM users WHERE id = ${id}) is SQL injection. Always use Prisma.sql tagged template literals.

Best Practices

  • Use select instead of include when you only need specific fields — it reduces query payload and improves performance.
  • Enable log: ['query'] in development to see the SQL Prisma generates and spot N+1 problems early.
  • Use prisma.$transaction for any operation spanning multiple models — it is the only way to guarantee consistency.
  • Use Prisma.UserGetPayload<...> utility types to type function return values without manually duplicating types.
  • Run prisma migrate deploy (not dev) in CI/CD pipelines to apply migrations without resetting the database.

Key Takeaways

  • Prisma generates a fully type-safe client from your schema — every query, filter, and relation is statically typed.
  • The global singleton pattern is essential in Next.js and other hot-reload environments to avoid connection exhaustion.
  • Use $transaction([...]) for sequential queries and $transaction(async (tx) => {...}) for interactive transactions that need to read within the transaction.
  • Prisma.sql tagged template literals are the only safe way to write raw queries — never interpolate user input directly.
  • select outperforms include when you need a subset of columns — it generates leaner SQL.
  • npx prisma migrate deploy is for production; npx prisma migrate dev is for local development only.
  • Prisma Accelerate provides connection pooling and caching as a managed service — important for serverless deployments.
  • Log Prisma queries in development (log: ['query']) to catch N+1 problems before they hit production.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading