Prisma is a TypeScript-first ORM that generates type-safe database clients from your schema definition. It eliminates the gap between your database and your TypeScript types, catching data access errors at compile time rather than at runtime.
What Prisma Is and Is Not
Prisma is not a query builder in the traditional sense (like Knex). It is not an ActiveRecord-style ORM where your model instances have methods that map to rows (like Sequelize). It is a schema-first data access layer: you define your models in a Prisma schema file, run prisma generate, and receive a type-safe client whose API exactly matches your schema.
The key insight: with Prisma, TypeScript knows the shape of every database query result. If you query for a user with their posts included, TypeScript knows that user.posts is an array of the Post type. If you forget to include posts and try to access user.posts, TypeScript will catch it at compile time.
The Three Packages
Understanding Prisma's package structure prevents confusion:
prisma (CLI): The development dependency. Provides prisma generate (regenerates the client after schema changes), prisma migrate (creates and runs migrations), prisma studio (local database browser), and prisma db push (pushes schema changes without creating migration files, for development).
@prisma/client (runtime): The production dependency. The generated client that your application imports and uses to query the database. Install this as a regular dependency, not devDependency.
@prisma/adapter-* (alternative drivers): Adapters for using alternative database drivers. @prisma/adapter-neon for Neon's serverless driver (HTTP-based Postgres for edge functions), @prisma/adapter-d1 for Cloudflare D1. Use these when the standard TCP connection does not work in your deployment environment.
Schema Definition
The Prisma schema (prisma/schema.prisma) defines your models, relations, and database configuration:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
}
After any schema change, run npx prisma generate to regenerate the client. In a project with CI/CD, add prisma generate to your build step.
Query Patterns
findMany with filters:
const posts = await prisma.post.findMany({
where: { published: true, author: { email: 'user@example.com' } },
orderBy: { createdAt: 'desc' },
take: 10,
skip: 20,
})
findUnique:
const user = await prisma.user.findUnique({
where: { email: 'user@example.com' },
include: { posts: { where: { published: true } } },
})
create:
const user = await prisma.user.create({
data: { email: 'new@example.com', name: 'Alice' },
})
upsert (create or update):
const user = await prisma.user.upsert({
where: { email: 'user@example.com' },
create: { email: 'user@example.com', name: 'Alice' },
update: { name: 'Alice Updated' },
})
Transactions:
const [post, auditLog] = await prisma.$transaction([
prisma.post.create({ data: { title: 'New post', authorId: userId } }),
prisma.auditLog.create({ data: { action: 'post.created', userId } }),
])
The N+1 Query Problem
The N+1 problem is a common performance issue with ORMs: you query for N records, then for each record you make an additional query to fetch related data. That is 1 + N queries instead of 1.
Prisma's include solves N+1 by fetching relations in the same query (or a batched second query). But include fetches all fields of the related records. For performance-sensitive endpoints, use select to fetch only the fields you need:
const users = await prisma.user.findMany({
select: {
id: true,
name: true,
posts: { select: { id: true, title: true } },
},
})
When to use raw SQL: for complex aggregations, window functions, or queries that Prisma's API cannot express efficiently, use prisma.$queryRaw:
const results = await prisma.$queryRaw`
SELECT author_id, COUNT(*) as post_count
FROM posts
WHERE published = true
GROUP BY author_id
ORDER BY post_count DESC
`
Migrations
Prisma Migrate creates SQL migration files from your schema changes. The workflow: edit schema, run prisma migrate dev --name add-published-flag, which generates a migration SQL file and applies it to your development database. In production, run prisma migrate deploy which applies pending migration files.
Migration files are committed to version control. This is important: your database schema history lives alongside your code history.
Prisma vs Drizzle vs Kysely
Prisma: schema-first, automatic type generation, best DX for teams who want to stay in Prisma's API. Higher runtime overhead, larger bundle size.
Drizzle: TypeScript-first, schema defined in TypeScript files, SQL-like query builder, smaller bundle, faster at runtime. Better for performance-sensitive apps and serverless (smaller bundle = lower cold start).
Kysely: type-safe SQL query builder without the schema-first approach. You bring your own type definitions. Maximum SQL control with TypeScript safety. Best for teams with strong SQL skills who do not want an ORM abstraction.
The rule of thumb: Prisma for teams who want the most productive DX with complex relation traversal. Drizzle for performance-sensitive apps or serverless deployments. Kysely for teams who want to write almost-raw SQL with type safety.
Keep Reading
- Drizzle ORM Guide — the SQL-first alternative to Prisma
- Postgres Guide for Developers — the database Prisma works best with
- GitHub Actions Guide for Developers — running Prisma migrations in CI/CD
Pristren builds AI-powered software for teams. Zlyqor is our all-in-one workspace — chat, projects, time tracking, AI meeting summaries, and invoicing — in one tool. Try it free.