Prisma: The Entity Framework of TypeScript
For .NET engineers who know: EF Core —
DbContext, migrations, LINQ queries,Include(),ThenInclude(),AsNoTracking(), andScaffold-DbContextYou’ll learn: How Prisma maps onto every EF Core concept you already know, and where the abstractions diverge in ways that will catch you off guard Time: 15-20 min read
The .NET Way (What You Already Know)
EF Core is a Code-First ORM (or Database-First via scaffolding). You define your domain model as C# classes, configure the mapping in DbContext (via fluent API or data annotations), generate migrations with Add-Migration, and query using LINQ. The full cycle:
// 1. Define the model
public class Order
{
public int Id { get; set; }
public decimal Total { get; set; }
public DateTime CreatedAt { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; }
public List<OrderItem> Items { get; set; } = new();
}
// 2. Configure DbContext
public class AppDbContext : DbContext
{
public DbSet<Order> Orders { get; set; }
public DbSet<Customer> Customers { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasOne(o => o.Customer)
.WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId);
modelBuilder.Entity<Order>()
.Property(o => o.Total)
.HasPrecision(12, 2);
}
}
// 3. Migrate
// dotnet ef migrations add AddOrders
// dotnet ef database update
// 4. Query
var orders = await context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.Where(o => o.Total > 100)
.OrderByDescending(o => o.CreatedAt)
.Take(20)
.AsNoTracking()
.ToListAsync();
This pattern — model, context, migration, query — is what Prisma replicates in TypeScript. The vocabulary is different but the structure is nearly isomorphic.
The Prisma Way
Installation
npm install prisma --save-dev
npm install @prisma/client
npx prisma init --datasource-provider postgresql
This creates:
prisma/schema.prisma— your schema file (replacesDbContext+ model classes).envwith aDATABASE_URLplaceholder
The Schema File: DbContext + Models Combined
In EF Core, you have separate C# files for each entity class plus a DbContext to configure them. In Prisma, everything lives in a single schema.prisma file: the database connection, generator configuration, and all model definitions.
// prisma/schema.prisma
// 1. Generator — what Prisma should generate
generator client {
provider = "prisma-client-js"
}
// 2. Datasource — your database connection
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// 3. Models — equivalent to your EF Core entity classes + OnModelCreating config
model Customer {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now()) @map("created_at")
orders Order[]
@@map("customers")
}
model Order {
id Int @id @default(autoincrement())
total Decimal @db.Decimal(12, 2)
createdAt DateTime @default(now()) @map("created_at")
customerId Int @map("customer_id")
customer Customer @relation(fields: [customerId], references: [id])
items OrderItem[]
@@map("orders")
}
model OrderItem {
id Int @id @default(autoincrement())
orderId Int @map("order_id")
productId Int @map("product_id")
quantity Int
price Decimal @db.Decimal(10, 2)
order Order @relation(fields: [orderId], references: [id])
product Product @relation(fields: [productId], references: [id])
@@map("order_items")
}
model Product {
id Int @id @default(autoincrement())
name String
sku String @unique
price Decimal @db.Decimal(10, 2)
orderItems OrderItem[]
@@map("products")
}
Schema attribute mapping:
| EF Core Configuration | Prisma Equivalent |
|---|---|
[Key] / .HasKey() | @id |
[DatabaseGenerated(Identity)] | @default(autoincrement()) |
[Required] / .IsRequired() | Field with no ? (non-optional) |
[MaxLength(200)] / .HasMaxLength() | @db.VarChar(200) |
.HasPrecision(12, 2) | @db.Decimal(12, 2) |
[Index(IsUnique = true)] | @unique or @@unique([field1, field2]) |
@@Index / .HasIndex() | @@index([field1, field2]) |
[Column("created_at")] | @map("created_at") |
[Table("orders")] | @@map("orders") |
HasOne().WithMany() | @relation() on the FK side |
Owned() / value objects | Not directly supported; use embedded JSON or separate table |
.HasDefaultValue() | @default(value) |
.HasDefaultValueSql("now()") | @default(now()) |
[Timestamp] / IsRowVersion() | No direct equivalent; use updatedAt DateTime @updatedAt |
Generating the Client: prisma generate
After editing the schema, you run:
npx prisma generate
This is equivalent to the EF Core model snapshot being updated. It reads your schema.prisma and generates a fully-typed TypeScript client in node_modules/@prisma/client. The generated client has:
- TypeScript types for every model
- Typed input/output types for every operation
- Typed filter, orderBy, and include arguments
The generated types are not something you write — they are entirely inferred from your schema. This is the most significant Prisma advantage: your query types are always in sync with your schema without any manual DTO definition.
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
That prisma instance gives you fully typed access to every model.
Migrations: prisma migrate
# Create a migration (equivalent to Add-Migration)
npx prisma migrate dev --name add_orders_table
# Apply to production (equivalent to Update-Database in CI/CD)
npx prisma migrate deploy
prisma migrate dev does three things:
- Generates a SQL migration file in
prisma/migrations/ - Applies it to your development database
- Regenerates the Prisma client (
prisma generate)
prisma migrate deploy applies pending migrations without generating or prompting — intended for CI/CD and production.
EF Core vs Prisma migration workflow:
| Step | EF Core | Prisma |
|---|---|---|
| Create migration | dotnet ef migrations add <Name> | npx prisma migrate dev --name <name> |
| Apply to dev | dotnet ef database update | (done automatically by migrate dev) |
| Apply to production | dotnet ef database update (or bundle) | npx prisma migrate deploy |
| View pending | dotnet ef migrations list | npx prisma migrate status |
| Rollback | dotnet ef database update <PreviousMigration> | Manual SQL (see article 5.4) |
| Reset dev DB | dotnet ef database drop && dotnet ef database update | npx prisma migrate reset |
| Scaffold from existing DB | dotnet ef dbcontext scaffold | npx prisma db pull |
Migration files are plain SQL stored in prisma/migrations/<timestamp>_<name>/migration.sql. Unlike EF Core’s C# migration files (with Up() and Down() methods), Prisma migration files are SQL-only and have no auto-generated Down(). See article 5.4 for rollback strategies.
Querying: Prisma Client vs EF Core LINQ
This is where the two ORMs feel most different in daily use. EF Core uses LINQ — method chaining that translates to SQL at runtime. Prisma uses a JavaScript object API where you pass nested objects describing your query.
EF Core vs Prisma API comparison:
| Operation | EF Core (C#) | Prisma (TypeScript) |
|---|---|---|
| Find by PK | context.Orders.FindAsync(id) | prisma.order.findUnique({ where: { id } }) |
| Find one (throws if missing) | context.Orders.SingleAsync(...) | prisma.order.findUniqueOrThrow({ where: { id } }) |
| Find first | context.Orders.FirstOrDefaultAsync(...) | prisma.order.findFirst({ where: {...} }) |
| Get all | context.Orders.ToListAsync() | prisma.order.findMany() |
| Filter | .Where(o => o.Total > 100) | findMany({ where: { total: { gt: 100 } } }) |
| Sort | .OrderByDescending(o => o.CreatedAt) | findMany({ orderBy: { createdAt: 'desc' } }) |
| Limit | .Take(20) | findMany({ take: 20 }) |
| Skip | .Skip(40) | findMany({ skip: 40 }) |
| Select fields | .Select(o => new { o.Id, o.Total }) | findMany({ select: { id: true, total: true } }) |
| Include relation | .Include(o => o.Customer) | findMany({ include: { customer: true } }) |
| Nested include | .ThenInclude(c => c.Address) | include: { customer: { include: { address: true } } } |
| Create | context.Orders.Add(order); await context.SaveChangesAsync() | prisma.order.create({ data: { ... } }) |
| Update by PK | context.Entry(order).State = Modified; | prisma.order.update({ where: { id }, data: { ... } }) |
| Upsert | .AddOrUpdate() (not built-in, use FindAsync + update) | prisma.order.upsert({ where, create, update }) |
| Delete | context.Orders.Remove(order) | prisma.order.delete({ where: { id } }) |
| Delete many | context.Orders.RemoveRange(...) | prisma.order.deleteMany({ where: { ... } }) |
| Count | context.Orders.CountAsync(...) | prisma.order.count({ where: { ... } }) |
| Aggregate | .SumAsync(o => o.Total) | prisma.order.aggregate({ _sum: { total: true } }) |
| Group by | .GroupBy(o => o.CustomerId) | prisma.order.groupBy({ by: ['customerId'], _sum: { total: true } }) |
Complete examples side by side:
// EF Core — get orders with customer and items, filtered and paginated
var orders = await context.Orders
.Include(o => o.Customer)
.Include(o => o.Items)
.ThenInclude(i => i.Product)
.Where(o => o.Total > 100 && o.CreatedAt >= DateTime.UtcNow.AddDays(-30))
.OrderByDescending(o => o.CreatedAt)
.Skip(page * pageSize)
.Take(pageSize)
.AsNoTracking()
.ToListAsync();
// Prisma — equivalent query
const orders = await prisma.order.findMany({
where: {
total: { gt: 100 },
createdAt: { gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) },
},
include: {
customer: true,
items: {
include: {
product: true,
},
},
},
orderBy: { createdAt: 'desc' },
skip: page * pageSize,
take: pageSize,
});
// EF Core — create with nested relation
var order = new Order
{
CustomerId = 1,
Total = 249.99m,
Items = new List<OrderItem>
{
new() { ProductId = 5, Quantity = 2, Price = 124.99m }
}
};
context.Orders.Add(order);
await context.SaveChangesAsync();
// Prisma — create with nested relation
const order = await prisma.order.create({
data: {
customerId: 1,
total: 249.99,
items: {
create: [
{ productId: 5, quantity: 2, price: 124.99 },
],
},
},
include: { items: true },
});
// EF Core — upsert pattern
var existing = await context.Customers
.FirstOrDefaultAsync(c => c.Email == email);
if (existing is null)
{
context.Customers.Add(new Customer { Email = email, Name = name });
}
else
{
existing.Name = name;
}
await context.SaveChangesAsync();
// Prisma — built-in upsert
const customer = await prisma.customer.upsert({
where: { email },
create: { email, name },
update: { name },
});
Filtering Reference
Prisma’s where clause uses nested operator objects. The mapping from SQL/LINQ:
// Equality
where: { status: 'active' }
// Comparison
where: { total: { gt: 100 } } // >
where: { total: { gte: 100 } } // >=
where: { total: { lt: 100 } } // <
where: { total: { lte: 100 } } // <=
where: { total: { not: 100 } } // !=
// String
where: { name: { contains: 'Smith' } } // LIKE '%Smith%'
where: { name: { startsWith: 'Jo' } } // LIKE 'Jo%'
where: { name: { endsWith: 'son' } } // LIKE '%son'
where: { email: { contains: '@', mode: 'insensitive' } } // ILIKE
// Null checks
where: { deletedAt: null } // IS NULL
where: { deletedAt: { not: null } } // IS NOT NULL
// In / Not In
where: { status: { in: ['active', 'pending'] } }
where: { status: { notIn: ['deleted'] } }
// AND (default when multiple keys at same level)
where: { total: { gt: 100 }, status: 'active' }
// OR
where: { OR: [{ status: 'active' }, { status: 'pending' }] }
// AND explicit
where: { AND: [{ total: { gt: 100 } }, { createdAt: { gte: startDate } }] }
// NOT
where: { NOT: { status: 'deleted' } }
// Relation filter — orders that have at least one item
where: { items: { some: { quantity: { gt: 0 } } } }
// Relation filter — all items satisfy condition
where: { items: { every: { price: { gt: 0 } } } }
// Relation filter — no items match condition
where: { items: { none: { quantity: 0 } } }
Pagination: Cursor vs Offset
// Offset pagination (like EF Core Skip/Take)
const page2 = await prisma.order.findMany({
skip: 20,
take: 10,
orderBy: { createdAt: 'desc' },
});
// Cursor pagination (more efficient for large datasets)
// First page — no cursor
const firstPage = await prisma.order.findMany({
take: 10,
orderBy: { id: 'asc' },
});
// Subsequent page — pass the last item's id as cursor
const nextPage = await prisma.order.findMany({
take: 10,
skip: 1, // skip the cursor record itself
cursor: { id: firstPage[firstPage.length - 1].id },
orderBy: { id: 'asc' },
});
Cursor pagination is more efficient for deep pages on large tables because it uses an indexed seek rather than counting rows. The tradeoff: you cannot jump to an arbitrary page — only forward and back.
Transactions
// EF Core transaction
using var transaction = await context.Database.BeginTransactionAsync();
try
{
context.Orders.Add(order);
await context.SaveChangesAsync();
context.Inventory.Update(inventoryUpdate);
await context.SaveChangesAsync();
await transaction.CommitAsync();
}
catch
{
await transaction.RollbackAsync();
throw;
}
// Prisma — interactive transaction (most flexible, like EF Core)
const [order, inventory] = await prisma.$transaction(async (tx) => {
const newOrder = await tx.order.create({
data: { customerId: 1, total: 99.99 },
});
const updatedInventory = await tx.inventory.update({
where: { productId: 5 },
data: { quantity: { decrement: 1 } },
});
return [newOrder, updatedInventory];
});
// Prisma — batch transaction (simpler, all succeed or all fail)
const [deleteOld, createNew] = await prisma.$transaction([
prisma.order.deleteMany({ where: { createdAt: { lt: cutoffDate } } }),
prisma.order.create({ data: { customerId: 1, total: 50 } }),
]);
The interactive transaction (async (tx) => { ... }) is the EF Core equivalent — you get a transaction-scoped client and can use any Prisma operations inside it. The batch transaction is a lighter syntax for a fixed list of operations.
Raw SQL
// EF Core raw SQL
var orders = await context.Orders
.FromSqlRaw("SELECT * FROM orders WHERE customer_id = {0}", customerId)
.ToListAsync();
// EF Core — raw SQL that doesn't return entities
await context.Database.ExecuteSqlRawAsync(
"UPDATE orders SET status = 'archived' WHERE created_at < {0}", cutoffDate);
// Prisma — raw query returning typed results
const orders = await prisma.$queryRaw<Order[]>`
SELECT * FROM orders WHERE customer_id = ${customerId}
`;
// Prisma — raw execute (no return value needed)
const count = await prisma.$executeRaw`
UPDATE orders SET status = 'archived' WHERE created_at < ${cutoffDate}
`;
// Prisma — raw with Prisma.sql for dynamic queries (safe parameterization)
import { Prisma } from '@prisma/client';
const minTotal = 100;
const orders = await prisma.$queryRaw<Order[]>(
Prisma.sql`SELECT * FROM orders WHERE total > ${minTotal}`
);
Always use tagged template literals (the backtick syntax) with $queryRaw and $executeRaw. Prisma automatically parameterizes the values, preventing SQL injection. If you need to interpolate SQL fragments dynamically (like a column name), use Prisma.raw() — but do so carefully and never with user input.
Seeding
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Upsert so seed is idempotent
const admin = await prisma.customer.upsert({
where: { email: 'admin@example.com' },
update: {},
create: {
email: 'admin@example.com',
name: 'Admin User',
},
});
console.log(`Seeded customer: ${admin.id}`);
}
main()
.catch(console.error)
.finally(() => prisma.$disconnect());
// package.json — register the seed script
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
npx prisma db seed
# Also runs automatically after: npx prisma migrate reset
Introspection: Scaffold-DbContext Equivalent
If you are inheriting an existing database (rather than building code-first), Prisma can generate the schema from the existing tables:
# Like Scaffold-DbContext — generates schema.prisma from your existing database
npx prisma db pull
# Then generate the client from the introspected schema
npx prisma generate
This fills schema.prisma with model definitions inferred from your tables. You will need to add relation fields manually (Prisma can infer them from foreign keys but the field names may need adjustment). After reviewing and editing the generated schema, you can begin using Prisma Client immediately.
Key Differences
| Concept | EF Core | Prisma |
|---|---|---|
| Schema definition | C# entity classes + DbContext | Single schema.prisma file |
| Client generation | Implicit — types are your C# classes | Explicit — prisma generate produces TS types |
| Migration files | C# Up()/Down() methods | Plain SQL files (no auto-rollback) |
| Query API | LINQ method chaining | Nested JS object API |
| Change tracking | Built-in (AsNoTracking() opt-out) | None — every query is stateless |
| Lazy loading | Supported (with proxies) | Not supported — always explicit include |
| N+1 prevention | Include() required | include required — same discipline |
| Raw SQL | FromSqlRaw(), ExecuteSqlRaw() | $queryRaw, $executeRaw |
| Transactions | BeginTransactionAsync() | $transaction() |
| Database-first | Scaffold-DbContext | prisma db pull |
| Seeding | HasData() in OnModelCreating + migrations | Separate seed.ts script |
| Connection pool | ADO.NET pool (managed by driver) | Built into Prisma Query Engine |
| Multiple DB support | Yes (different providers) | Separate datasource blocks (experimental) |
Gotchas for .NET Engineers
1. The Prisma Query Engine is a Rust binary — cold starts matter in serverless
When you run prisma generate, Prisma downloads a Rust-compiled query engine binary for your platform. This binary handles connection pooling and query execution. In a long-running process (a traditional Node.js API server), this is invisible. In serverless environments (AWS Lambda, Vercel Edge Functions, Cloudflare Workers), the binary adds cold start time and can cause deployment size issues.
For serverless, use Prisma Accelerate (Prisma’s connection pool proxy service) or PgBouncer separately, and consider the DATABASE_URL pointing to a pooler endpoint rather than the direct database:
# Direct connection (fine for long-running servers)
DATABASE_URL="postgresql://user:pass@host:5432/db"
# PgBouncer / Prisma Accelerate (for serverless)
DATABASE_URL="postgresql://user:pass@pgbouncer-host:6432/db?pgbouncer=true&connection_limit=1"
The connection_limit=1 tells Prisma not to try to maintain a pool itself (the pooler handles it).
2. N+1 is completely invisible without include
EF Core supports lazy loading (though it is opt-in and generally discouraged). Prisma does not. If you access a relation that you did not include, you get null or undefined — not an extra query.
// This returns orders with customer = null (not loaded)
const orders = await prisma.order.findMany();
for (const order of orders) {
console.log(order.customer); // undefined — not an automatic extra query
}
// This is the N+1 anti-pattern if done manually
const orders = await prisma.order.findMany();
for (const order of orders) {
// This is a separate DB query per order — N+1
const customer = await prisma.customer.findUnique({
where: { id: order.customerId },
});
}
// Correct — use include
const orders = await prisma.order.findMany({
include: { customer: true },
});
The silver lining: Prisma’s design makes N+1 obvious and explicit. You will not accidentally trigger lazy loading through a navigation property access. But you must plan your include structure upfront.
3. findUnique vs findFirst — the semantics are different
This trips up developers expecting something like FirstOrDefaultAsync to just work everywhere.
// findUnique — ONLY works on fields marked @id or @unique in schema
// Uses a unique index — fast
const customer = await prisma.customer.findUnique({
where: { id: 42 }, // OK — @id
});
const customer2 = await prisma.customer.findUnique({
where: { email: 'foo@example.com' }, // OK — @unique
});
// This does NOT compile — 'name' is not @id or @unique
const customer3 = await prisma.customer.findUnique({
where: { name: 'Chris' }, // TypeScript error
});
// findFirst — works on any field, returns first match (or null)
// May do a full table scan if field is not indexed
const customer4 = await prisma.customer.findFirst({
where: { name: 'Chris' },
orderBy: { createdAt: 'desc' },
});
findUnique gives you a TypeScript-level guarantee that you are querying on a unique field and that the result is either the record or null (never a list). findFirst is the general-purpose single-record fetch. Use findUnique whenever the field is @id or @unique — it is both faster and more semantically correct.
4. Prisma does not track changes — every update requires explicit fields
EF Core’s change tracker lets you load an entity, mutate its properties, and call SaveChangesAsync(). Prisma has no change tracker. Every update call must specify the new values explicitly.
// EF Core — mutate and save
var customer = await context.Customers.FindAsync(id);
customer.Name = "New Name";
customer.UpdatedAt = DateTime.UtcNow;
await context.SaveChangesAsync(); // EF Core computes the UPDATE from change tracking
// Prisma — must specify every field you want to change
const customer = await prisma.customer.update({
where: { id },
data: {
name: 'New Name',
updatedAt: new Date(),
},
});
// You cannot do this:
const customer = await prisma.customer.findUnique({ where: { id } });
customer.name = 'New Name'; // mutating the object does nothing to the database
await prisma.customer.save(customer); // this method does not exist
This is actually safer in many respects — there are no accidental implicit saves — but it requires a mindset shift if you are used to the entity-mutation pattern.
5. Decimal requires special handling in TypeScript
PostgreSQL’s NUMERIC/DECIMAL type maps to Prisma’s Decimal type, which uses the decimal.js library internally. This is not a native JavaScript number — it is an object.
// Prisma returns Decimal objects for Decimal fields, not numbers
const order = await prisma.order.findUnique({ where: { id: 1 } });
console.log(typeof order.total); // 'object', not 'number'
console.log(order.total.toString()); // '249.99'
console.log(order.total.toNumber()); // 249.99 (native JS number, precision loss risk)
// Comparison
if (order.total.greaterThan(100)) { ... } // correct
if (order.total > 100) { ... } // does not work as expected
if (order.total.toNumber() > 100) { ... } // works but loses precision for large values
// When creating/updating, pass a string or Decimal
await prisma.order.create({
data: {
total: new Decimal('249.99'), // or just '249.99' — Prisma accepts strings
customerId: 1,
},
});
For financial data, treat Decimal values as opaque objects and use the decimal.js API for arithmetic. Serialize them to strings for API responses (total.toString()), not toNumber().
Hands-On Exercise
Build a typed data access layer for a blog system.
Schema:
// 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[]
createdAt DateTime @default(now()) @map("created_at")
@@map("users")
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
authorId Int @map("author_id")
author User @relation(fields: [authorId], references: [id])
tags Tag[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@index([authorId])
@@map("posts")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
@@map("tags")
}
npx prisma migrate dev --name init
Exercise — implement these functions:
// src/blog-repository.ts
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
// 1. Get published posts with author, paginated, optional tag filter
export async function getPublishedPosts(options: {
page: number;
pageSize: number;
tag?: string;
}) {
const { page, pageSize, tag } = options;
return prisma.post.findMany({
where: {
published: true,
...(tag ? { tags: { some: { name: tag } } } : {}),
},
include: {
author: { select: { id: true, name: true } },
tags: true,
},
orderBy: { createdAt: 'desc' },
skip: page * pageSize,
take: pageSize,
});
}
// 2. Create a post with tags (create tags if they don't exist)
export async function createPost(data: {
title: string;
content: string;
authorId: number;
tags: string[];
}) {
return prisma.post.create({
data: {
title: data.title,
content: data.content,
authorId: data.authorId,
tags: {
connectOrCreate: data.tags.map((name) => ({
where: { name },
create: { name },
})),
},
},
include: { tags: true, author: true },
});
}
// 3. Publish a post — return null if not found
export async function publishPost(id: number) {
try {
return await prisma.post.update({
where: { id },
data: { published: true },
});
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2025') {
return null; // Record not found
}
throw e;
}
}
// 4. Get post counts grouped by author
export async function getPostCountByAuthor() {
return prisma.post.groupBy({
by: ['authorId'],
_count: { id: true },
orderBy: { _count: { id: 'desc' } },
});
}
Quick Reference
# Setup
npm install prisma --save-dev && npm install @prisma/client
npx prisma init --datasource-provider postgresql
# Development cycle
npx prisma migrate dev --name <description> # create + apply migration
npx prisma generate # regenerate client after schema change
npx prisma db pull # introspect existing DB into schema
npx prisma db push # push schema to DB without migration (prototyping)
npx prisma migrate reset # drop DB, re-apply all migrations, seed
npx prisma migrate status # list applied/pending migrations
npx prisma studio # open browser-based data editor
# Production
npx prisma migrate deploy # apply pending migrations (no prompts)
npx prisma db seed # run seed script
// Common query patterns
import { PrismaClient, Prisma } from '@prisma/client';
const prisma = new PrismaClient();
// Instantiation (singleton pattern for production)
// lib/prisma.ts
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
// CRUD
await prisma.model.findUnique({ where: { id } })
await prisma.model.findUniqueOrThrow({ where: { id } })
await prisma.model.findFirst({ where: { ... } })
await prisma.model.findMany({ where, orderBy, skip, take, include, select })
await prisma.model.create({ data: { ... }, include: { ... } })
await prisma.model.update({ where: { id }, data: { ... } })
await prisma.model.upsert({ where, create, update })
await prisma.model.delete({ where: { id } })
await prisma.model.deleteMany({ where: { ... } })
await prisma.model.count({ where: { ... } })
await prisma.model.aggregate({ where, _sum, _avg, _min, _max, _count })
await prisma.model.groupBy({ by: ['field'], _count: true, orderBy })
// Transactions
await prisma.$transaction(async (tx) => { await tx.model.create(...) })
await prisma.$transaction([op1, op2, op3])
// Raw SQL
await prisma.$queryRaw<T[]>`SELECT * FROM table WHERE id = ${id}`
await prisma.$executeRaw`UPDATE table SET col = ${val} WHERE id = ${id}`
// Error handling
import { Prisma } from '@prisma/client';
if (e instanceof Prisma.PrismaClientKnownRequestError) {
e.code // 'P2002' = unique constraint, 'P2025' = record not found
}
// Common error codes
// P2002 — Unique constraint failed
// P2003 — Foreign key constraint failed
// P2025 — Record to update/delete not found