Toolmingo
Guides11 min read

Prisma Cheat Sheet: ORM for TypeScript and Node.js

Complete Prisma ORM reference — schema syntax, CRUD operations, relations, filtering, pagination, transactions, migrations, and raw queries. With TypeScript examples.

Prisma is a next-generation ORM for TypeScript and Node.js that makes database access type-safe and intuitive. This cheat sheet covers everything from schema definition to advanced queries.

Quick reference

Task Code
Install npm i prisma @prisma/client
Init npx prisma init
Generate client npx prisma generate
Create migration npx prisma migrate dev --name init
Apply migrations (prod) npx prisma migrate deploy
Open Prisma Studio npx prisma studio
Reset DB npx prisma migrate reset
Pull schema from DB npx prisma db pull
Push schema (no migration) npx prisma db push
Seed database npx prisma db seed
Format schema npx prisma format
Validate schema npx prisma validate

Schema syntax

Datasource and generator

datasource db {
  provider = "postgresql" // mysql | sqlite | sqlserver | mongodb | cockroachdb
  url      = env("DATABASE_URL")
}

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

Model definition

model User {
  id        Int      @id @default(autoincrement())
  uuid      String   @id @default(uuid())          // alternative: uuid PK
  email     String   @unique
  name      String?                                 // optional field
  role      Role     @default(USER)
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  posts     Post[]                                  // relation

  @@index([email, role])                            // composite index
  @@map("users")                                    // custom table name
}

enum Role {
  USER
  ADMIN
  MODERATOR
}

Field types

Prisma Type PostgreSQL MySQL SQLite
String TEXT/VARCHAR TEXT/VARCHAR TEXT
Int INTEGER INT INTEGER
BigInt BIGINT BIGINT INTEGER
Float DOUBLE PRECISION DOUBLE REAL
Decimal DECIMAL DECIMAL DECIMAL
Boolean BOOLEAN TINYINT(1) INTEGER
DateTime TIMESTAMP DATETIME DATETIME
Json JSONB JSON TEXT
Bytes BYTEA LONGBLOB BLOB

Field attributes

model Post {
  id        Int      @id @default(autoincrement())
  slug      String   @unique
  title     String   @db.VarChar(255)     // native type
  views     Int      @default(0)
  published Boolean  @default(false)
  authorId  Int
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade)
  tags      Tag[]    @relation("PostToTag")
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@unique([authorId, slug])               // composite unique
  @@fulltext([title, content])             // MySQL full-text index
}

Relations

// One-to-many (User has many Posts)
model User {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model Post {
  id       Int  @id @default(autoincrement())
  authorId Int
  author   User @relation(fields: [authorId], references: [id])
}

// Many-to-many (implicit join table)
model Post {
  id   Int   @id @default(autoincrement())
  tags Tag[]
}

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

// Many-to-many (explicit join table)
model PostTag {
  postId    Int
  tagId     Int
  assignedBy String
  post      Post @relation(fields: [postId], references: [id])
  tag       Tag  @relation(fields: [tagId], references: [id])

  @@id([postId, tagId])
}

// One-to-one
model User {
  id      Int      @id @default(autoincrement())
  profile Profile?
}

model Profile {
  id     Int  @id @default(autoincrement())
  userId Int  @unique
  user   User @relation(fields: [userId], references: [id])
}

// Self-relation (categories with parent)
model Category {
  id       Int        @id @default(autoincrement())
  parentId Int?
  parent   Category?  @relation("CategoryTree", fields: [parentId], references: [id])
  children Category[] @relation("CategoryTree")
}

Prisma Client setup

// lib/prisma.ts — singleton pattern (important for dev hot-reload)
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }

export const prisma = globalForPrisma.prisma ?? new PrismaClient({
  log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
})

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

CRUD operations

Create

// Create one
const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    role: 'ADMIN',
  },
})

// Create with nested relation
const post = await prisma.post.create({
  data: {
    title: 'Hello World',
    author: {
      connect: { email: 'alice@example.com' },   // connect existing
      // create: { email: 'new@example.com' },    // or create new
    },
    tags: {
      connectOrCreate: [
        { where: { name: 'tech' }, create: { name: 'tech' } },
      ],
    },
  },
  include: { author: true, tags: true },
})

// Create many (no return by default)
await prisma.user.createMany({
  data: [
    { email: 'bob@example.com', name: 'Bob' },
    { email: 'carol@example.com', name: 'Carol' },
  ],
  skipDuplicates: true,
})

// createManyAndReturn (Prisma 5.14+)
const users = await prisma.user.createManyAndReturn({
  data: [{ email: 'dave@example.com' }],
})

Read

// Find unique (throws if not found)
const user = await prisma.user.findUniqueOrThrow({ where: { id: 1 } })

// Find unique (null if not found)
const user = await prisma.user.findUnique({ where: { email: 'alice@example.com' } })

// Find first matching
const post = await prisma.post.findFirst({
  where: { published: true },
  orderBy: { createdAt: 'desc' },
})

// Find many
const posts = await prisma.post.findMany({
  where: {
    published: true,
    author: { role: 'ADMIN' },          // filter via relation
  },
  orderBy: [
    { createdAt: 'desc' },
    { title: 'asc' },
  ],
  take: 10,
  skip: 20,
  include: {
    author: { select: { name: true, email: true } },
    tags: true,
    _count: { select: { comments: true } },   // count relation
  },
})

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

Update

// Update one (by unique field)
const user = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Alice Smith' },
})

// Update many
const { count } = await prisma.post.updateMany({
  where: { published: false, createdAt: { lt: new Date('2024-01-01') } },
  data: { published: true },
})

// Upsert (update or create)
const user = await prisma.user.upsert({
  where: { email: 'alice@example.com' },
  update: { name: 'Alice Updated' },
  create: { email: 'alice@example.com', name: 'Alice' },
})

// Atomic number updates
await prisma.post.update({
  where: { id: 1 },
  data: { views: { increment: 1 } },    // also: decrement, multiply, divide, set
})

// Nested update
await prisma.user.update({
  where: { id: 1 },
  data: {
    posts: {
      create: { title: 'New Post' },             // create nested
      update: { where: { id: 5 }, data: { title: 'Updated' } },
      disconnect: { id: 3 },                     // remove relation
      deleteMany: { published: false },          // delete nested records
    },
  },
})

Delete

// Delete one
await prisma.user.delete({ where: { id: 1 } })

// Delete many
const { count } = await prisma.post.deleteMany({
  where: { createdAt: { lt: new Date('2023-01-01') } },
})

Filtering

// Comparison operators
where: {
  age: { gt: 18, lte: 65 },         // gt, gte, lt, lte
  name: { not: null },              // not null
  email: { in: ['a@b.com', 'c@d.com'] },
  role: { notIn: ['BANNED'] },
}

// String filters
where: {
  email: { contains: '@gmail.com' },
  name:  { startsWith: 'Al' },
  slug:  { endsWith: '-draft' },
  title: { contains: 'prisma', mode: 'insensitive' },  // case-insensitive
}

// Logical operators
where: {
  AND: [
    { published: true },
    { createdAt: { gte: new Date('2024-01-01') } },
  ],
  OR: [
    { role: 'ADMIN' },
    { email: { contains: '@company.com' } },
  ],
  NOT: { banned: true },
}

// Relation filters
where: {
  // has at least one post
  posts: { some: { published: true } },
  // all posts are published
  posts: { every: { published: true } },
  // has no posts
  posts: { none: {} },
  // nested scalar filter
  posts: { some: { views: { gt: 100 } } },
}

// Null checks
where: {
  deletedAt: null,        // IS NULL
  deletedAt: { not: null }, // IS NOT NULL
}

Pagination

// Offset pagination
const PAGE_SIZE = 10

async function getPage(page: number) {
  const [users, total] = await prisma.$transaction([
    prisma.user.findMany({
      take: PAGE_SIZE,
      skip: (page - 1) * PAGE_SIZE,
      orderBy: { createdAt: 'desc' },
    }),
    prisma.user.count(),
  ])
  return { users, total, pages: Math.ceil(total / PAGE_SIZE) }
}

// Cursor pagination (better performance for large tables)
async function getNextPage(cursor?: number) {
  const items = await prisma.post.findMany({
    take: 10,
    ...(cursor && { skip: 1, cursor: { id: cursor } }),
    orderBy: { id: 'asc' },
  })
  const nextCursor = items.length === 10 ? items[9].id : null
  return { items, nextCursor }
}

Aggregations

// Count
const count = await prisma.user.count({ where: { role: 'ADMIN' } })

// Aggregate
const stats = await prisma.post.aggregate({
  where: { published: true },
  _count: true,
  _sum: { views: true },
  _avg: { views: true },
  _min: { createdAt: true },
  _max: { views: true },
})
// stats._avg.views -> number | null

// Group by
const groups = await prisma.post.groupBy({
  by: ['authorId', 'published'],
  _count: { id: true },
  _sum: { views: true },
  having: { views: { _sum: { gt: 100 } } },
  orderBy: { _sum: { views: 'desc' } },
})

Transactions

// Sequential transaction
const [payment, order] = await prisma.$transaction([
  prisma.payment.create({ data: { amount: 100 } }),
  prisma.order.update({ where: { id: 1 }, data: { status: 'PAID' } }),
])

// Interactive transaction (with business logic)
await prisma.$transaction(async (tx) => {
  const sender = await tx.account.update({
    where: { id: senderId },
    data: { balance: { decrement: amount } },
  })

  if (sender.balance < 0) {
    throw new Error('Insufficient funds')   // auto-rollback on throw
  }

  await tx.account.update({
    where: { id: receiverId },
    data: { balance: { increment: amount } },
  })
})

// Transaction with timeout and isolation level
await prisma.$transaction(
  async (tx) => { /* ... */ },
  { timeout: 10000, isolationLevel: 'Serializable' }
)

Raw queries

// Raw SQL query
const users = await prisma.$queryRaw<User[]>`
  SELECT * FROM users
  WHERE created_at > ${new Date('2024-01-01')}
  ORDER BY name ASC
`

// Raw execute (INSERT/UPDATE/DELETE)
const { count } = await prisma.$executeRaw`
  UPDATE posts SET views = views + 1 WHERE id = ${postId}
`

// Unsafe raw (when you need dynamic SQL — avoid if possible)
const tableName = 'users'
const result = await prisma.$queryRawUnsafe(
  `SELECT * FROM ${tableName} LIMIT 10`
)

Middleware and extensions (Prisma 4.7+)

// Soft delete extension
const xprisma = prisma.$extends({
  model: {
    $allModels: {
      async softDelete<T>(this: T, id: number) {
        const context = Prisma.getExtensionContext(this)
        return (context as any).update({
          where: { id },
          data: { deletedAt: new Date() },
        })
      },
    },
  },
  query: {
    $allModels: {
      async findMany({ args, query }) {
        args.where = { ...args.where, deletedAt: null }   // auto-filter soft-deleted
        return query(args)
      },
    },
  },
})

await xprisma.user.softDelete(1)

Common mistakes

Mistake Problem Fix
new PrismaClient() in every request Exhausts DB connection pool Use singleton pattern in lib/prisma.ts
No onDelete on required relations Migration fails or FK constraint error Add onDelete: Cascade or SetNull
findMany without take Returns entire table Always paginate with take
updateMany without where Updates all rows Always include a where clause
deleteMany without where Deletes all rows Always include a where clause
Forgetting include on relations null instead of related data Add include: { relation: true }
Using $queryRawUnsafe with user input SQL injection Use tagged template literal $queryRaw with params
Not awaiting prisma.$transaction Partial commits Always await transactions

Migrations workflow

# Development: create and apply migration
npx prisma migrate dev --name add_user_role

# Review generated SQL before applying
cat prisma/migrations/20240101_add_user_role/migration.sql

# Production: apply pending migrations only (no dev features)
npx prisma migrate deploy

# Check migration status
npx prisma migrate status

# Reset database (dev only — drops all data)
npx prisma migrate reset

# Mark a migration as applied without running it (when manually applied)
npx prisma migrate resolve --applied 20240101_add_user_role

Prisma with Next.js

// app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function GET() {
  const users = await prisma.user.findMany({
    select: { id: true, name: true, email: true },
    take: 50,
    orderBy: { createdAt: 'desc' },
  })
  return NextResponse.json(users)
}

export async function POST(request: Request) {
  const body = await request.json()
  const user = await prisma.user.create({
    data: { email: body.email, name: body.name },
  })
  return NextResponse.json(user, { status: 201 })
}

Seeding

// prisma/seed.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  const alice = await prisma.user.upsert({
    where: { email: 'alice@example.com' },
    update: {},
    create: {
      email: 'alice@example.com',
      name: 'Alice',
      role: 'ADMIN',
      posts: {
        create: [
          { title: 'Hello World', published: true },
          { title: 'Getting started with Prisma', published: false },
        ],
      },
    },
  })
  console.log({ alice })
}

main()
  .catch(console.error)
  .finally(() => prisma.$disconnect())
// package.json
{
  "prisma": {
    "seed": "ts-node prisma/seed.ts"
  }
}
npx prisma db seed

FAQ

Q: Should I use findUnique or findFirst when filtering by a non-unique field? Use findFirst for non-unique fields — findUnique only works with fields marked @unique or @id. findFirst accepts any where clause but returns only one result.

Q: How do I handle the N+1 problem in Prisma? Use include or select with nested relations instead of looping and fetching in N queries. For deeply nested or computed relations, use prisma.$transaction with multiple queries or raw SQL with a JOIN.

Q: What's the difference between connect, connectOrCreate, and create in nested writes?

  • create — creates a new related record
  • connect — links to an existing record by unique identifier
  • connectOrCreate — links if exists, creates if not (upsert for relations)

Q: Can I use Prisma with an existing database? Yes — run npx prisma db pull to introspect the existing schema into schema.prisma, then npx prisma generate to generate the client. Use prisma migrate diff to manage future changes.

Q: How do I handle optional vs required relations in Prisma? A required relation (no ?) means the foreign key is NOT NULL. An optional relation (Model?) allows NULL. Use onDelete: SetNull with optional relations to avoid constraint errors when the parent is deleted.

Q: Is Prisma suitable for production at scale? Yes — Prisma is used in production by many large companies. For high-throughput scenarios, tune the connection pool (connection_limit in the URL), use Prisma Accelerate for edge caching, and avoid N+1 patterns. For very complex queries, drop to raw SQL with $queryRaw.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools