Prisma changed how I think about database operations. Instead of writing raw SQL or dealing with complex ORM abstractions, Prisma gives you a type-safe, intuitive API that feels natural. It's like having autocomplete for your entire database schema, and TypeScript catches errors before they reach production.
I've worked with a lot of database tools over the years—raw SQL, Sequelize, TypeORM, you name it. Each had its strengths, but they all felt like they were fighting against me in some way. Then I discovered Prisma, and everything clicked. The type safety, the intuitive API, the automatic migrations—it all just makes sense.
Prisma is a next-generation ORM that gives you a type-safe database client. You define your schema in a simple, declarative format, and Prisma generates a fully-typed client for you. No more guessing what fields exist, no more runtime errors from typos, no more manual migration management.
What is Prisma ORM?
Prisma is a next-generation ORM (Object-Relational Mapping) tool for Node.js and TypeScript. It provides:
- Type-safe database client - Full TypeScript support with autocomplete
- Automatic migrations - Schema changes are tracked and applied automatically
- Intuitive query API - Simple, chainable methods for all operations
- Multi-database support - PostgreSQL, MySQL, SQLite, SQL Server, and MongoDB
- Relationship handling - One-to-one, one-to-many, and many-to-many relationships
- Raw SQL support - Run raw queries when needed with SQL injection protection
Installation and Setup
First, let's install Prisma:
npm install prisma @prisma/client --save-dev
npm install @prisma/adapter-pg pg # For PostgreSQL
Initialize Prisma in your project:
npx prisma init
This creates a prisma directory with a schema.prisma file and adds a .env file for your database connection string.
Defining Your Schema
The schema file is where you define your database structure. Here's a complete example with all relationship types:
// prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User Model
model User {
id String @id @default(uuid())
age Int
name String
email String @unique
role Role @default(BASIC)
// One-to-Many: User has many Posts
writtenPosts Post[] @relation("WrittenPosts")
// One-to-One: User has one UserPreference
userPreference UserPreference? @relation(fields: [userPreferenceId], references: [id])
userPreferenceId String? @unique
@@unique([age, name]) // Composite unique constraint
@@index([email]) // Index on email
}
// One-to-One Relationship
model UserPreference {
id String @id @default(uuid())
emailUpdates Boolean
user User? // One-to-one relationship with User
}
// One-to-Many Relationship
model Post {
id String @id @default(uuid())
title String
averageRating Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Post belongs to User (author)
author User @relation("WrittenPosts", fields: [authorId], references: [id])
authorId String
// Many-to-Many: Post has many Categories
categories Category[]
}
// Many-to-Many Relationship
model Category {
id String @id @default(uuid())
name String @unique
posts Post[] // Many-to-many relationship with Post
}
enum Role {
BASIC
ADMIN
USER
}
After defining your schema, generate the Prisma Client and run migrations:
npx prisma generate
npx prisma migrate dev --name init
Setting Up Prisma Client
Create a singleton instance of Prisma Client to avoid multiple connections in development:
// lib/prisma.ts
import "dotenv/config";
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '../generated/prisma/client';
const connectionString = process.env.NEON_POSTGRES_DATABASE_URL!;
// Prevent multiple instances in development
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
adapter: new PrismaPg(pgPool),
log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
});
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
Basic CRUD Operations
Create
// Create a single record
const user = await prisma.user.create({
data: {
name: "John Doe",
email: "john@example.com",
age: 30,
},
});
Read
// Find unique record
const user = await prisma.user.findUnique({
where: { email: "john@example.com" },
});
// Find many with filters
const users = await prisma.user.findMany({
where: { age: { gte: 18 } },
orderBy: { createdAt: "desc" },
take: 10,
});
Update
// Update a single record
const user = await prisma.user.update({
where: { id: "user-id" },
data: { age: 31 },
});
Delete
// Delete a single record
const user = await prisma.user.delete({
where: { id: "user-id" },
});
Best Practices
- Use Prisma Client singleton
- Define relationships properly
- Use transactions for complex operations
- Handle errors gracefully
- Use migrations for schema changes
- Leverage TypeScript types
📖 Read the Complete Guide
This is just a brief overview! The complete guide on my blog includes:
- ✅ Relationships - One-to-one, one-to-many, many-to-many
- ✅ Advanced Queries - Filtering, sorting, pagination
- ✅ Transactions - Complex database operations
- ✅ Migrations - Schema management
- ✅ Raw SQL - When to use raw queries
- ✅ Real-world examples from production applications
👉 Read the full article with all code examples here
What's your experience with Prisma? Share your tips in the comments! 🚀
For more backend guides, check out my blog covering Express.js, JWT Authentication, Sequelize, and more.
Top comments (0)