Always define both sides of a relation to keep your schema clear and maintainable:
prisma/schema.prisma
model User {
id Int @id @default(autoincrement())
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
author User @relation(fields: [authorId], references: [id])
authorId Int
}
[!WARNING]
For databases that don't enforce foreign keys (like PlanetScale), Prisma ORM emulates relations and you should manually add indexes on relation scalar fields to avoid full table scans:
prisma/schema.prisma
model Comment {
postId Int
post Post @relation(fields: [postId], references: [id])
@@index([postId])
}
Index fields used in where, orderBy, and relations. Without indexes, the database can be forced to scan entire tables to find matching rows, which becomes slower as tables grow.
prisma/schema.prisma
model Comment {
id Int @id @default(autoincrement())
postId Int
status String
post Post @relation(fields: [postId], references: [id])
@@index([postId])
@@index([status])
}
For large projects, use multi-file Prisma schemas (available since v6.7.0):
prisma/
├── schema.prisma # Main schema with generator and datasource
├── migrations/ # Migration files
├── user.prisma # User-related models
├── product.prisma # Product-related models
└── order.prisma # Order-related models
The schema.prisma file (containing the generator block) and migrations/ directory must be at the same level. You can also group additional schema files under a subdirectory such as prisma/models/.
Create one global PrismaClient instance and reuse it throughout your application. Creating multiple instances creates multiple connection pools, which can exhaust your database's connection limit and slow down queries.
The N+1 problem occurs when you run 1 query to fetch a list, then 1 additional query per item in that list. This creates many unnecessary round-trips to the database instead of a few efficient queries.
n-plus-one.ts
// ❌ Bad: N+1 queries (1 + N queries)const users = await prisma.user.findMany()
for (const user of users) {
const posts = await prisma.post.findMany({
where: { authorId: user.id }
})
}
// ✅ Good: Single query with includeconst users = await prisma.user.findMany({
include: { posts: true }
})
// ✅ Good: Batch with IN filterconst users = await prisma.user.findMany()
const posts = await prisma.post.findMany({
where: { authorId: { in: users.map(u => u.id) } }
})
Use cursor-based pagination for large datasets or infinite scroll. Cursor-based pagination scales better because it uses indexed columns to find the starting position instead of traversing skipped rows:
Bulk operations (createMany, createManyAndReturn, updateMany, updateManyAndReturn, and deleteMany) automatically run as transactions, so all writes either succeed together or are rolled back if something fails.
Prisma ORM's API is safe by default. For raw queries, always use parameterized queries. String concatenation with untrusted input allows attackers to inject arbitrary SQL into your queries.
sql-injection-prevention.ts
// ✅ Safe: tagged templateconst result = await prisma.$queryRaw`
SELECT * FROM "User" WHERE email = ${email}
`// ✅ Safe: parameterizedconst result = await prisma.$queryRawUnsafe(
'SELECT * FROM "User" WHERE email = $1',
email
)
// ❌ Unsafe: string concatenationconst query = `SELECT * FROM "User" WHERE email = '${email}'`const result = await prisma.$queryRawUnsafe(query)
Use prisma migrate dev to create and apply migrations
Use prisma db push only for quick prototyping (may reset data)
Production:
Use onlyprisma migrate deploy with committed migrations
Never use migrate dev (can prompt to reset DB) or db push (can be destructive and locks you into a migrationless workflow)
prisma migrate deploy applies existing migrations in a non-interactive way, uses advisory locking to prevent concurrent runs, and is safe for production data.
Creating a new client inside the handler on every invocation risks exhausting database connections. Each concurrent function creates its own connection pool, quickly multiplying connection counts.
Dev environment: Set up your local Prisma ORM development environment, including editor tooling and environment variables.
ORM releases and maturity levels: Learn about the release process, versioning, and maturity of Prisma ORM components and how to deal with breaking changes that might happen throughout releases