Prisma Reference: Schema, CRUD, Relations, Raw Queries, Transactions & Error Handling
Prisma is a TypeScript ORM with a schema-first workflow: you define your data model in schema.prisma, run prisma generate, and get a fully-typed client. The key advantage over raw SQL is auto-completion and compile-time type safety on every query. The trade-offs: the query API doesn’t map 1:1 to SQL, complex queries require raw SQL via $queryRaw, and the generated client adds ~5ms cold start in serverless.
1. Schema, Migration & Client Setup
schema.prisma models, data types, @relation, and prisma migrate dev workflow
// schema.prisma:
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // postgresql | mysql | sqlite | mongodb
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt // auto-set on every update
posts Post[] // one-to-many relation
profile Profile? // one-to-one (optional)
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[] @relation("PostTags") // many-to-many
@@index([authorId]) // add index
}
// Migrations:
npx prisma migrate dev --name add_posts // create + apply migration (dev)
npx prisma migrate deploy // apply migrations (CI/prod, no prompt)
npx prisma db push // push schema changes without migration file (prototyping)
npx prisma studio // open GUI to browse data
// Client:
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient({
log: ["query", "warn", "error"], // log queries in dev
});
// Singleton pattern for Next.js / serverless (avoid multiple instances):
// export const prisma = global.prisma ?? new PrismaClient();
// if (process.env.NODE_ENV !== "production") global.prisma = prisma;
2. CRUD Queries
create, findMany with where/select/include, update, upsert, delete, and transactions
// Create:
const user = await prisma.user.create({
data: { name: "Alice", email: "alice@example.com" },
select: { id: true, name: true, email: true }, // only return these fields
});
// findMany with filtering:
const users = await prisma.user.findMany({
where: {
email: { contains: "@example.com" },
createdAt: { gte: new Date("2026-01-01") },
posts: { some: { published: true } }, // has at least one published post
},
orderBy: { createdAt: "desc" },
skip: 0,
take: 20,
include: { posts: { where: { published: true }, orderBy: { createdAt: "desc" } } },
});
// findUnique (throws if not found — use findFirst for "maybe"):
const user = await prisma.user.findUnique({ where: { email: "alice@example.com" } });
const user = await prisma.user.findUniqueOrThrow({ where: { id: 1 } });
// Update:
const updated = await prisma.user.update({
where: { id: 1 },
data: { name: "Alice Smith", updatedAt: new Date() },
});
// Upsert (create or update):
const user = await prisma.user.upsert({
where: { email: "alice@example.com" },
update: { name: "Alice" },
create: { email: "alice@example.com", name: "Alice" },
});
// Delete:
await prisma.user.delete({ where: { id: 1 } });
await prisma.user.deleteMany({ where: { createdAt: { lt: new Date("2025-01-01") } } });
// Count:
const count = await prisma.user.count({ where: { posts: { some: {} } } });
3. Relations — include, select, nested writes
Eager load with include/select, nested create/connect, and N+1 awareness
// include: eager load relations (generates JOIN or extra SELECT):
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { published: true },
orderBy: { createdAt: "desc" },
take: 5,
select: { id: true, title: true }, // only post fields needed
},
profile: true,
},
});
// select vs include (can't use both on same level):
const user = await prisma.user.findMany({
select: {
id: true,
name: true,
_count: { select: { posts: true } }, // count of relations
},
});
// Nested create (create user + posts in one call):
await prisma.user.create({
data: {
name: "Bob",
email: "bob@example.com",
posts: {
create: [
{ title: "First post" },
{ title: "Second post", published: true },
],
},
},
});
// Connect existing records (many-to-many):
await prisma.post.update({
where: { id: 1 },
data: {
tags: { connect: [{ id: 5 }, { id: 6 }] }, // add tags
// disconnect: [{ id: 3 }] // remove tag
// set: [{ id: 5 }] // replace all tags
},
});
4. Raw Queries, Aggregations & Middleware
$queryRaw for complex SQL, groupBy, aggregate, and Prisma middleware for logging/soft-delete
// $queryRaw: use when Prisma's query API isn't enough:
const result = await prisma.$queryRaw`
SELECT u.name, COUNT(p.id) AS post_count
FROM "User" u
LEFT JOIN "Post" p ON p."authorId" = u.id
WHERE u."createdAt" > ${new Date("2026-01-01")}
GROUP BY u.id, u.name
ORDER BY post_count DESC
`;
// Template literals are parameterized — safe from SQL injection
// Don't use $queryRawUnsafe with user input
// groupBy + aggregate:
const stats = await prisma.post.groupBy({
by: ["authorId"],
_count: { id: true },
_avg: { viewCount: true },
having: { viewCount: { _avg: { gt: 100 } } },
orderBy: { _count: { id: "desc" } },
});
// Prisma middleware (runs on every query):
prisma.$use(async (params, next) => {
const before = Date.now();
const result = await next(params);
const after = Date.now();
if (after - before > 500) {
console.warn(`Slow query: ${params.model}.${params.action} took ${after - before}ms`);
}
return result;
});
// Soft-delete pattern with middleware:
prisma.$use(async (params, next) => {
if (params.model === "Post" && params.action === "delete") {
return next({ ...params, action: "update", args: { ...params.args, data: { deletedAt: new Date() } } });
}
if (params.action === "findMany") {
params.args.where = { ...params.args?.where, deletedAt: null };
}
return next(params);
});
5. Transactions, Error Handling & Performance
$transaction, PrismaClientKnownRequestError, connection pooling, and Accelerate
// Transaction (all succeed or all rollback):
const [user, post] = await prisma.$transaction([
prisma.user.create({ data: { name: "Alice", email: "alice@example.com" } }),
prisma.post.create({ data: { title: "Hello", authorId: 1 } }),
]);
// Interactive transaction (with logic between queries):
await prisma.$transaction(async (tx) => {
const from = await tx.account.findUniqueOrThrow({ where: { id: fromId } });
if (from.balance < amount) throw new Error("Insufficient funds");
await tx.account.update({ where: { id: fromId }, data: { balance: { decrement: amount } } });
await tx.account.update({ where: { id: toId }, data: { balance: { increment: amount } } });
});
// Error handling:
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
try {
await prisma.user.create({ data: { email: "dupe@example.com" } });
} catch (e) {
if (e instanceof PrismaClientKnownRequestError) {
if (e.code === "P2002") {
throw new Error(`Unique constraint violation on: ${e.meta?.target}`);
}
if (e.code === "P2025") {
throw new Error("Record not found");
}
}
throw e;
}
// Common error codes: P2002 unique, P2003 FK violation, P2025 not found
// Connection pooling (serverless — pgBouncer or Prisma Accelerate):
// DATABASE_URL="postgresql://...?pgbouncer=true&connection_limit=1"
// Or: Prisma Accelerate (edge-optimized connection pooler + global cache)
// Always disconnect in scripts:
prisma.$disconnect();
Track Node.js and Prisma releases at ReleaseRun. Related: Next.js App Router Reference | TypeScript Reference | PostgreSQL Advanced SQL Reference | TypeScript EOL Tracker
🔍 Free tool: npm Package Health Checker — check prisma, @prisma/client, and related packages for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com