ORM

Prisma ORM: Modern Database Access Guide

P

Prasanna Joshi

Author

November 15, 2024
14 min read

Introduction to Prisma

Prisma is a next-generation ORM that consists of the following tools:

  • Prisma Client: Auto-generated, type-safe query builder for Node.js & TypeScript.
  • Prisma Migrate: Migration system.
  • Prisma Studio: GUI to view and edit data in your database.

The Schema File

Everything starts in schema.prisma:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User    @relation(fields: [authorId], references: [id])
  authorId  Int
}

Type-Safe Queries

Prisma generates a client based on your schema, giving you incredible autocomplete:

const user = await prisma.user.findUnique({
  where: { email: 'alice@prisma.io' },
  include: { posts: true }, // Typed result includes posts!
});

Advanced Prisma Patterns

Using Select for Partial Queries

Instead of fetching entire records, use select to only fetch the fields you need. This improves performance and reduces the data transferred:

// Select only specific fields
const userEmails = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
    // Omit name and other fields
  },
})

// With relations - select nested fields
const usersWithPostTitles = await prisma.user.findMany({
  select: {
    name: true,
    posts: {
      select: {
        title: true,
        publishedDate: true,
        // Don't fetch full post content
      },
    },
  },
})

Filtering with Complex Conditions

Prisma's filter API makes building complex queries intuitive and type-safe:

// Complex filtering with AND, OR, NOT
const recentPublishedPosts = await prisma.post.findMany({
  where: {
    AND: [
      { published: true },
      { publishedDate: { gte: new Date('2024-01-01') } },
      {
        OR: [
          { title: { contains: 'TypeScript' } },
          { content: { contains: 'Prisma' } },
        ],
      },
    ],
  },
  orderBy: { publishedDate: 'desc' },
  take: 10,
})

// Finding posts without a specific tag
const postsWithoutPrisma = await prisma.post.findMany({
  where: {
    NOT: { tags: { has: 'Prisma' } },
  },
})

Relationships and Includes

Managing relationships is one of Prisma's strongest features. You can easily load related data with full type safety:

// One-to-many relationship
const userWithPosts = await prisma.user.findUnique({
  where: { id: userId },
  include: {
    posts: {
      select: { id: true, title: true },
      orderBy: { publishedDate: 'desc' },
      take: 5,
    },
  },
})

// Many-to-many relationships
model Post {
  id    Int     @id @default(autoincrement())
  title String
  tags  Tag[]
}

model Tag {
  id    Int     @id @default(autoincrement())
  name  String
  posts Post[]
}

// Query with nested includes
const postsWithTags = await prisma.post.findMany({
  include: {
    tags: {
      select: { name: true },
    },
    author: {
      select: { name: true, email: true },
    },
  },
})

Prisma Migrations - Managing Schema Changes

Creating Your First Migration

Migrations track changes to your database schema over time. When you modify your schema, Prisma generates migration files:

# Create a new migration
npx prisma migrate dev --name add_user_role

# This will:
# 1. Create a migration file in prisma/migrations
# 2. Apply it to your dev database
# 3. Regenerate Prisma Client

Migration Files

Migration files contain SQL that describes exactly what changed. You can review and modify them before applying:

-- prisma/migrations/add_user_role/migration.sql
ALTER TABLE "User" ADD COLUMN "role" VARCHAR(255) NOT NULL DEFAULT 'user';
ALTER TABLE "User" CREATE INDEX "User_role_idx" on ("role");

Handling Migrations in Production

For production deployments, use migrate deploy which applies unapplied migrations without generating new ones:

# In CI/CD pipeline
npx prisma migrate deploy

# This applies pending migrations and is safe for production

Performance Optimization Techniques

Using Pagination

Always paginate large datasets to avoid loading too much data at once:

async function getPaginatedPosts(page: number, pageSize: number = 10) {
  const skip = (page - 1) * pageSize
  
  const [posts, total] = await Promise.all([
    prisma.post.findMany({
      skip,
      take: pageSize,
      orderBy: { publishedDate: 'desc' },
    }),
    prisma.post.count(),
  ])
  
  return {
    posts,
    pageInfo: {
      total,
      page,
      pageSize,
      pages: Math.ceil(total / pageSize),
    },
  }
}

Batch Operations

Use createMany and updateMany for bulk operations to reduce database round trips:

// Create multiple records efficiently
const newUsers = await prisma.user.createMany({
  data: [
    { email: 'alice@example.com', name: 'Alice' },
    { email: 'bob@example.com', name: 'Bob' },
    { email: 'charlie@example.com', name: 'Charlie' },
  ],
  skipDuplicates: true, // Skip if email already exists
})

// Update many records
const updated = await prisma.post.updateMany({
  where: { published: false },
  data: { published: true },
})

Raw Database Access

When Prisma's query API isn't enough, you can execute raw SQL while still getting type safety with prepared statements:

// Using $queryRaw for SELECT queries
const result = await prisma.$queryRaw`
  SELECT u.id, u.name, COUNT(p.id) as post_count
  FROM "User" u
  LEFT JOIN "Post" p ON u.id = p."authorId"
  WHERE u.email = ${userEmail}
  GROUP BY u.id
`

// Using $executeRaw for INSERT/UPDATE/DELETE
await prisma.$executeRaw`
  UPDATE "Post" SET "updatedAt" = NOW() 
  WHERE "authorId" = ${userId}
`

Debugging with Prisma Studio

Prisma Studio provides a visual interface for viewing and editing your database:

npx prisma studio

# Opens browser at http://localhost:5555
# Allows you to:
# - Browse all records
# - Create new records
# - Edit existing records
# - Delete records
# - Filter and search

Common Prisma Patterns

Soft Deletes

Instead of permanently deleting records, mark them as deleted:

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  deletedAt DateTime?
}
// Soft delete a post
await prisma.post.update({
  where: { id: postId },
  data: { deletedAt: new Date() },
})

// Query active posts only
const activePosts = await prisma.post.findMany({
  where: { deletedAt: null },
})

Timestamps

Automatically track when records are created and updated:

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
// updatedAt is automatically set on every update
const updated = await prisma.post.update({
  where: { id: postId },
  data: { title: 'New Title' },
  // updatedAt is automatically set to current time
})

Validation at the Schema Level

Use Prisma's validation features to enforce constraints at the database level:

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique // Prevents duplicate emails
  name  String
  age   Int?
  posts Post[]
  
  // Database-level constraints
  @@index([email]) // For faster lookups
  @@unique([name, email]) // Multiple fields
}

Best Practices for Production

  • Always use select or include: Only fetch the data you actually need.
  • Implement pagination: Never load all records without pagination.
  • Use database indexes: Index frequently queried fields for better performance.
  • Test migrations thoroughly: Always test migrations on staging before deploying to production.
  • Monitor slow queries: Use Prisma's query logging to identify slow queries.
  • Use connection pooling: Configure connection pooling for production databases.
  • Handle transactions carefully: Use Prisma transactions for operations that must succeed or fail together.

Troubleshooting Common Issues

Connection Issues

If Prisma can't connect to your database, check your DATABASE_URL environment variable and ensure the database is running.

Type Generation

If types aren't updating, regenerate Prisma Client:

npx prisma generate

Migration Conflicts

If your migration history gets out of sync with the database, reset your dev database and reapply migrations:

npx prisma migrate reset

Conclusion

Prisma combines the convenience of an ORM with the power of type safety and SQL clarity. Its intuitive API, excellent developer experience, and strong typing make it an excellent choice for modern Node.js applications. Whether you're building a small prototype or a large-scale application, Prisma's features scale with your needs. Start with simple queries, leverage type safety, and gradually adopt advanced patterns as your application grows.

Tags

#Prisma#Database#TypeScript#SQL

Share this article

About the Author

P

Prasanna Joshi

Expert Writer & Developer

Prasanna Joshi is an experienced software engineer and technology writer passionate about helping developers master modern web technologies. With years of professional experience in full-stack development, system design, and best practices, they bring real-world insights to every article.

Specializing in Next.js, TypeScript, Node.js, databases, and web performance optimization. Follow for more in-depth technical content.

Stay Updated

Get the latest articles delivered to your inbox