TL;DR: An ORM (Object-Relational Mapping) lets you interact with a database using JavaScript or TypeScript objects instead of writing raw SQL. Prisma and Drizzle are the two most popular ORMs in the AI coding ecosystem. They handle creating tables, querying data, and updating your database schema — but they can hide performance problems if you do not understand what SQL they generate underneath.
Why AI Coders Need to Know This
If you have built anything with a database using AI tools in 2025 or 2026, the AI almost certainly generated ORM code. According to the 2025 State of JS survey, Prisma is used by 28% of JavaScript developers, and Drizzle is the fastest-growing ORM in the ecosystem. When you prompt Cursor or Claude Code with "add a database to my app," the response is overwhelmingly likely to include Prisma schema files, migration commands, and ORM-generated queries.
This creates a specific problem for vibe coders: you have database code in your project that you did not write and might not understand. The ORM generates SQL behind the scenes, runs it against your database, and returns JavaScript objects. When it works, it feels like magic. When it breaks — a migration fails, a query returns the wrong data, or your app slows to a crawl — you are stuck because you cannot see what is actually happening.
Understanding ORMs at a practical level gives you three powers:
- Read what AI generated. You can look at a Prisma schema and know what tables and relationships it creates.
- Debug when queries fail. You know where to look when data comes back wrong or a migration errors out.
- Prompt more effectively. Instead of "add a database," you can say "add a Prisma schema with a User model that has many Posts" and get exactly what you need.
The Core Idea: Tables as Objects
A relational database stores data in tables with rows and columns. To get data out, you write SQL:
SELECT id, name, email FROM users WHERE id = 1;
An ORM lets you write the same thing in JavaScript:
const user = await prisma.user.findUnique({
where: { id: 1 },
select: { id: true, name: true, email: true }
});
Both produce the same result: a single user record with id, name, and email. The difference is that the ORM version uses your programming language's syntax, returns a typed JavaScript object, and handles connection pooling, escaping, and query building for you.
Think of it like a translator. You speak JavaScript. The database speaks SQL. The ORM translates between them so you do not have to switch languages every time you need data.
Real Scenario
You are building a blog platform with Cursor. You want users, posts, and comments. You ask the AI to set up the database layer.
Prompt I Would Type
Set up Prisma with PostgreSQL for a blog app:
- User model: id, name, email (unique), createdAt
- Post model: id, title, content, published (boolean), authorId (relates to User)
- Comment model: id, text, authorId (relates to User), postId (relates to Post)
- Add proper indexes on foreign keys
- Generate the migration and show me example queries for:
1. Create a user
2. Get all published posts with author names
3. Get a post with all its comments
This prompt is specific about the models, relationships, and the queries you want to see. The more structure you give the AI, the closer the first output is to what you actually need.
What AI Generated
First, the Prisma schema file (prisma/schema.prisma):
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
createdAt DateTime @default(now())
posts Post[] // One user has many posts
comments Comment[] // One user has many comments
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[] // One post has many comments
@@index([authorId]) // Index for faster lookups by author
}
model Comment {
id Int @id @default(autoincrement())
text String
createdAt DateTime @default(now())
author User @relation(fields: [authorId], references: [id])
authorId Int
post Post @relation(fields: [postId], references: [id])
postId Int
@@index([authorId])
@@index([postId])
}
Then the example queries:
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// 1. Create a user
const newUser = await prisma.user.create({
data: {
name: 'Chuck',
email: 'chuck@example.com'
}
});
// 2. Get all published posts with author names
const posts = await prisma.post.findMany({
where: { published: true },
include: {
author: {
select: { name: true }
}
}
});
// 3. Get a post with all its comments
const postWithComments = await prisma.post.findUnique({
where: { id: 1 },
include: {
comments: {
include: {
author: { select: { name: true } }
}
}
}
});
Understanding Each Part
The schema file
The schema.prisma file is the single source of truth for your database structure. It defines:
- Models — Each
modelbecomes a database table.model User→userstable. - Fields — Each line inside a model becomes a column.
name String→ a text column called "name." - Relations — Lines like
posts Post[]and@relation(fields: [authorId])define how tables connect to each other. - Attributes —
@id,@unique,@default, and@@indexadd database constraints and optimizations.
When you run npx prisma migrate dev, Prisma reads this schema, compares it to the current database, and generates SQL migration files to make the database match.
Migrations
A migration is a version-controlled database change. When you add a field to your Prisma schema and run prisma migrate dev, it creates a SQL file like:
-- Migration: add bio to users
ALTER TABLE "users" ADD COLUMN "bio" TEXT;
This file gets saved in prisma/migrations/ and tracked by Git. Migrations let you evolve your database structure over time without losing data and without manually writing SQL. They are also how you deploy database changes to production — you run pending migrations and the database updates.
The Prisma Client
PrismaClient is the auto-generated JavaScript/TypeScript client that gives you methods for every model. After you run npx prisma generate, you get:
prisma.user.findMany()— get multiple usersprisma.user.findUnique()— get one user by unique fieldprisma.user.create()— insert a new userprisma.user.update()— modify an existing userprisma.user.delete()— remove a user
Every method is fully typed — your editor knows exactly what fields exist and what types they expect. This is why AI tools love Prisma: the type safety means the AI can generate correct queries with less guesswork.
Relations and includes
The include option in Prisma queries is how you fetch related data. include: { author: true } tells Prisma to also fetch the related User record when getting a Post. Under the hood, this generates a SQL JOIN or a separate query.
This is powerful but can be dangerous. If you include everything on every query, you fetch far more data than you need. The AI often does this — it includes all relations by default to make the example complete. In production, only include what the specific page or API endpoint needs.
Prisma vs. Drizzle: Which One Will AI Generate?
Prisma
Philosophy: Declarative schema, auto-generated client, migrations built in.
Best for: Beginners, rapid prototyping, teams that want less SQL.
AI default? Yes — most AI tools generate Prisma first.
Gotcha: Can generate inefficient SQL for complex queries. Heavier runtime.
Drizzle
Philosophy: SQL-like syntax in TypeScript, lightweight, close to raw SQL.
Best for: Developers who want SQL control with TypeScript safety.
AI default? Growing — Claude Code and Cursor increasingly suggest it.
Gotcha: Steeper learning curve, less documentation than Prisma.
As a vibe coder, you will encounter both. If the AI generates Prisma, you will see schema.prisma files and prisma.model.method() calls. If it generates Drizzle, you will see TypeScript schema definitions and db.select().from(users).where(eq(users.id, 1)) calls that look more like SQL translated to JavaScript.
What AI Gets Wrong About ORMs
The N+1 query problem
This is the most common performance issue in ORM-generated code. Say you want to display 50 blog posts with their author names. Bad ORM usage:
// ❌ N+1 problem — 1 query for posts + 50 queries for authors
const posts = await prisma.post.findMany();
for (const post of posts) {
const author = await prisma.user.findUnique({
where: { id: post.authorId }
});
post.authorName = author.name;
}
That is 51 database queries. The fix:
// ✅ Single query with include — 1-2 queries total
const posts = await prisma.post.findMany({
include: { author: { select: { name: true } } }
});
AI generates the N+1 pattern when it writes loops that fetch related data inside the loop instead of using includes or joins. Watch for any await inside a for loop that queries the database.
Missing indexes
AI often generates relations without adding indexes on foreign key columns. The schema "works" but queries get slower as your data grows. Every authorId or postId field that gets used in WHERE clauses or JOINs should have a @@index.
Forgetting to run migrations
AI modifies the Prisma schema but does not remind you to run npx prisma migrate dev. Your code expects a new column but the database does not have it yet. Error: "column does not exist."
Exposing the Prisma client in API responses
Prisma objects include internal metadata. AI sometimes returns the raw Prisma result from an API endpoint, which can leak data like password hashes or internal IDs that should not be exposed. Always select only the fields you need:
// ❌ Exposes everything including internal fields
res.json(await prisma.user.findUnique({ where: { id: 1 } }));
// ✅ Select only what the API consumer needs
res.json(await prisma.user.findUnique({
where: { id: 1 },
select: { id: true, name: true, email: true }
}));
Not handling connection pooling
AI creates new PrismaClient() inside API route handlers or functions. Each invocation creates a new database connection. In serverless environments, this exhausts the connection pool fast. The fix:
// lib/prisma.ts — single instance, reused everywhere
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient };
export const prisma = globalForPrisma.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
ORM Performance Rule
If you see await prisma.something.findUnique() inside a for loop, you have an N+1 problem. Refactor to use include or findMany with a where: { id: { in: [...ids] } } pattern.
How to Debug ORM Issues With AI
Enable query logging
Prisma can log every SQL query it generates:
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error']
});
This shows you the actual SQL being executed. When a query returns unexpected results, the SQL log tells you whether the ORM translated your intent correctly.
Read migration files
When a migration fails, open the SQL file in prisma/migrations/. The SQL is usually straightforward. Common issues: trying to add a NOT NULL column to a table with existing data, renaming a column that other code references, or conflicting migration states.
The debugging prompt
Debug Prompt
My Prisma query is returning [wrong result / error].
Here's my schema: [paste relevant models]
Here's the query: [paste the Prisma call]
Here's the error/output: [paste exact error or unexpected result]
What's wrong and how do I fix it?
When to use raw SQL instead
ORMs are great for CRUD operations. They struggle with complex aggregations, reporting queries, full-text search, and bulk operations. Both Prisma and Drizzle let you drop to raw SQL when needed:
// Prisma raw query
const result = await prisma.$queryRaw`
SELECT author_id, COUNT(*) as post_count
FROM posts
WHERE published = true
GROUP BY author_id
HAVING COUNT(*) > 5
`;
Do not feel bad about using raw SQL for complex queries. The ORM handles 90% of your database interactions. For the other 10%, raw SQL is the right tool.
What to Learn Next
- What Is a Database? — Understand what the ORM is abstracting over.
- What Is SQL? — Learn the language your ORM translates to.
- What Is PostgreSQL? — The most common database Prisma connects to.
- What Is an API? — ORMs power the data layer behind your API routes.
Next Step
The next time AI generates Prisma code, enable query logging and watch the SQL it produces. Understanding that translation is the fastest way to go from "the ORM is magic" to "the ORM is a tool I control."
FAQ
ORM stands for Object-Relational Mapping. It is a technique that lets you interact with a relational database using your programming language's objects and methods instead of writing raw SQL queries.
Prisma is more beginner-friendly with its declarative schema and auto-generated client. Drizzle gives you more SQL-like control with full TypeScript integration. For vibe coders starting out, Prisma is usually the easier choice. If you want to stay closer to raw SQL while keeping type safety, Drizzle is worth learning.
Yes. ORMs abstract SQL, but they do not eliminate the need to understand it. When queries perform poorly, when migrations fail, or when the ORM generates unexpected SQL, knowing basic SQL helps you diagnose the problem and give better instructions to AI tools.
A migration is a version-controlled change to your database schema. When you modify your ORM schema file — add a table, add a column, rename a field — you generate a migration that contains the SQL needed to update the actual database. Migrations are tracked in your codebase so changes are repeatable and reversible.
Yes. The most common issue is the N+1 query problem — the ORM makes one query per related record instead of a single join. Other issues include over-fetching data with eager loading, missing database indexes, and creating too many database connections in serverless environments. Enabling query logging helps you catch these problems early.