You know SQL now, and it's powerful. Writing raw SQL strings inside your TypeScript code is kind of a pain though. You get zero auto-complete and zero type checking, and a typo can sit there quietly until your app is actually running. Prisma fixes all of that by giving you a type-safe way to talk to your database in plain TypeScript.
What is an ORM?
An ORM (Object-Relational Mapping) is a tool that lets you work with your database in your own programming language. You write TypeScript, and Prisma quietly turns it into optimized SQL behind the scenes.
Example
// Without Prisma (raw SQL - no type safety!)
const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
// With Prisma (type-safe, auto-complete, error checking!)
const user = await prisma.user.findUnique({
where: { email },
});
Installing Prisma (Pin It to Version 7)
This lesson uses Prisma 7, which is the version the Prisma docs call fully supported right now. Prisma 8 is out as a release candidate, and here's the gotcha. A plain npm install prisma now grabs the version 8 tool, which works totally differently and doesn't have the commands in this lesson. So pin version 7 on purpose.
That last command creates your schema file, a .env file and a small Prisma config file (more on that in a sec).
The Prisma Schema
Everything starts with the Prisma schema file (prisma/schema.prisma). That's where you define your database models, which are basically your tables.
Example
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
age Int?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
body String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
Here's what each piece of that schema is doing.
generator tells Prisma to generate a TypeScript client, and output says which folder to put it in
datasource says what kind of database you're using (PostgreSQL here)
model defines a table along with its columns and relationships
@id marks the primary key
@unique adds a unique constraint
@default sets a default value
Int? uses the ? to say the field is optional (nullable)
Post[] sets up a one-to-many relationship (a user has many posts)
@relation spells out how models connect through foreign keys
So where does the database address go? Not in the schema anymore. It lives in that Prisma config file prisma init made for you (newer 7.x releases name it prisma7.config.ts, older ones prisma.config.ts, and Prisma finds either one).
The actual connection string goes in your .env file as DATABASE_URL, so the password stays out of your code.
Migrations: Evolving Your Database
Whenever you change your Prisma schema, your actual database has to catch up. That's the job of migrations.
Example
# Create a migration after changing your schema
npx prisma migrate dev --name add-posts-table
# This does two things:
# 1. Creates a SQL migration file you can review
# 2. Applies the migration to your development database
# Then regenerate the Prisma client so your types match
npx prisma generate
Don't skip that second command. In Prisma 7, migrate dev no longer regenerates the client for you, and if you forget, your code won't know about the new columns. (ask me how i know)
Migration files get saved in prisma/migrations/ and you should commit them to Git. That way you've got a complete history of every change your database ever went through.
Setting Up Prisma Client
Once your schema is defined and you've run npx prisma generate, create an instance of the client. In Prisma 7 you hand it a driver adapter, which is the lil piece that actually talks to PostgreSQL.
Example
// lib/db.ts
import "dotenv/config";
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });
export default prisma;
See how it imports from ../generated/prisma/client? That's the output folder from your schema, not the @prisma/client package older tutorials use.
Now you can import prisma anywhere in your app and start talking to the database.
CRUD Operations with Prisma
Create (Insert)
Example
// Create a single user
const user = await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@example.com',
age: 28,
},
});
// user.id is automatically set!
// Create a user with a post at the same time
const userWithPost = await prisma.user.create({
data: {
name: 'Bob',
email: 'bob@example.com',
posts: {
create: {
title: 'My First Post',
body: 'Hello world!',
},
},
},
include: { posts: true },
});
Read (Query)
Example
// Find one user by unique field
const user = await prisma.user.findUnique({
where: { email: 'alice@example.com' },
});
// Find many users with filtering
const activeUsers = await prisma.user.findMany({
where: {
age: { gte: 18 },
posts: { some: { published: true } },
},
orderBy: { createdAt: 'desc' },
take: 10, // LIMIT 10
});
// Include related data
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: { posts: true },
});
Update
Example
// Update a single user
const updated = await prisma.user.update({
where: { id: 1 },
data: { name: 'Alice Smith' },
});
// Update many records at once
const count = await prisma.post.updateMany({
where: { authorId: 1, published: false },
data: { published: true },
});
Delete
Example
// Delete a single user
await prisma.user.delete({
where: { id: 1 },
});
// Delete many records
await prisma.post.deleteMany({
where: { published: false },
});
Filtering with Prisma
Prisma gives you type-safe filter operators that map straight onto SQL.
Prisma is a type-safe ORM that turns your TypeScript into SQL
The Prisma schema (schema.prisma) defines your models, relationships and constraints
Use prisma migrate dev to apply schema changes while you're developing, then prisma generate to refresh the client
Pin Prisma to version 7 (prisma@7) while Prisma 8 is still a release candidate
The CRUD operations are create, findMany/findUnique, update and delete
Filter with type-safe operators like gt, contains, in, NOT and OR
Pull in related data with include, or trim it down with select
Use upsert when you need "create or update"
Transactions ($transaction) make sure several operations succeed or fail together
What's Next?
You've got raw SQL AND Prisma under your belt now. In the last lesson of this chapter you'll pick up database best practices... indexing for speed, normalization for clean data, protection against SQL injection and backup plans that keep your data safe.