The Complete Prisma Guide | ORM, Schema, Migration, Queries, Type Safety, Hands-on

The Complete Prisma Guide | ORM, Schema, Migration, Queries, Type Safety, Hands-on

What this post covers

This is a complete guide to type-safe database work with Prisma. It walks through schema definition, migrations, CRUD, relations, and Prisma Studio with practical examples.

From the field: Migrating from TypeORM to Prisma, we gained stronger type safety and cut time spent writing queries by about 50%.

Introduction: “ORMs feel too complex”

Real-world scenarios

Scenario 1: Weak type safety

You keep hitting runtime errors. Prisma gives you end-to-end strong typing. Scenario 2: Migrations are painful

You end up doing manual steps. Prisma supports automated migrations from your schema. Scenario 3: Queries get unwieldy

You fall back to raw SQL. Prisma offers a straightforward API.


1. What is Prisma?

Core characteristics

Prisma is a next-generation ORM. Key benefits:

  • Type safety: first-class TypeScript support
  • Automatic migrations: driven by your schema
  • Prisma Studio: GUI for browsing and editing data
  • Intuitive API: simpler query authoring
  • Performance: optimized query execution

2. Installation and setup

Install

npm install prisma @prisma/client
npx prisma init

.env

DATABASE_URL="postgresql://user:password@localhost:5432/mydb"

Schema definition

Below is a detailed Prisma-based setup. Read through the code while noting what each part does.

// prisma/schema.prisma
generator client {
  provider = "prisma-client-js"
}
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}
model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  posts     Post[]
  profile   Profile?
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
model Post {
  id        Int      @id @default(autoincrement())
  title     String
  content   String?
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
model Profile {
  id     Int     @id @default(autoincrement())
  bio    String?
  user   User    @relation(fields: [userId], references: [id])
  userId Int     @unique
}

Migration

npx prisma migrate dev --name init
npx prisma generate

3. CRUD operations

Create

import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Create a single record
const user = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'John',
  },
});
// Create with nested relations
const userWithPost = await prisma.user.create({
  data: {
    email: '[email protected]',
    name: 'Jane',
    posts: {
      create: [
        { title: 'First Post', content: 'Hello World' },
        { title: 'Second Post', content: 'Another Post' },
      ],
    },
  },
  include: {
    posts: true,
  },
});

Read

// Single record by id
const user = await prisma.user.findUnique({
  where: { id: 1 },
});
// Multiple records
const users = await prisma.user.findMany({
  where: {
    email: {
      contains: '@example.com',
    },
  },
  orderBy: {
    createdAt: 'desc',
  },
  take: 10,
});
// With relations
const userWithPosts = await prisma.user.findUnique({
  where: { id: 1 },
  include: {
    posts: {
      where: { published: true },
    },
    profile: true,
  },
});

Update

// Update one record
const updatedUser = await prisma.user.update({
  where: { id: 1 },
  data: {
    name: 'John Updated',
  },
});
// Update many records
const result = await prisma.user.updateMany({
  where: {
    email: {
      contains: '@example.com',
    },
  },
  data: {
    name: 'Updated',
  },
});

Delete

The example below uses TypeScript with async/await for efficient I/O. Review each part to see what it does.

// Delete one
const deletedUser = await prisma.user.delete({
  where: { id: 1 },
});
// Delete many
const result = await prisma.user.deleteMany({
  where: {
    email: {
      contains: '@test.com',
    },
  },
});

4. Advanced queries

Aggregation

This TypeScript example uses async/await. Run it yourself to see the behavior.

const result = await prisma.post.aggregate({
  _count: { id: true },
  _avg: { authorId: true },
  _sum: { authorId: true },
  _min: { createdAt: true },
  _max: { createdAt: true },
});

Group By

This TypeScript example uses async/await. Read through the code while noting the role of each part.

const result = await prisma.post.groupBy({
  by: ['authorId'],
  _count: {
    id: true,
  },
  having: {
    id: {
      _count: {
        gt: 5,
      },
    },
  },
});

Transaction

const [user, post] = await prisma.$transaction([
  prisma.user.create({
    data: { email: '[email protected]', name: 'John' },
  }),
  prisma.post.create({
    data: { title: 'First Post', authorId: 1 },
  }),
]);
// Interactive transaction
await prisma.$transaction(async (tx) => {
  const user = await tx.user.create({
    data: { email: '[email protected]', name: 'Jane' },
  });
  await tx.post.create({
    data: { title: 'Post', authorId: user.id },
  });
});

5. Prisma Studio

npx prisma studio

Features:

  • Browse and edit data
  • GUI interface
  • Visualize relations
  • Quick manual testing

6. Performance optimization

Connection pool

The example below configures PrismaClient in TypeScript with logging enabled so queries, warnings, and errors surface clearly—helpful for debugging and safer operation in development.

const prisma = new PrismaClient({
  datasources: {
    db: {
      url: process.env.DATABASE_URL,
    },
  },
  log: ['query', 'info', 'warn', 'error'],
});

Select optimization

This TypeScript example uses async/await. Run it to verify behavior.

// Fetch only the fields you need
const users = await prisma.user.findMany({
  select: {
    id: true,
    email: true,
  },
});

Job search and interviews

Topics like schema design, migrations, and avoiding N+1 queries are common in backend interviews. Pair the database and API question flow in Tech Interview Preparation Guide with the JD-matching section in Developer Resume & Interview Guide to explain how you used Prisma in projects.


Summary and checklist

Key takeaways

  • Prisma: a next-generation ORM
  • Type safety: strong TypeScript support
  • Schema: declarative models
  • Migration: generated from schema changes
  • Prisma Studio: GUI management
  • Performance: optimized queries

Implementation checklist

  • Install Prisma
  • Define the schema
  • Run migrations
  • Implement CRUD
  • Configure relations
  • Add transactions
  • Tune performance

  • The Complete NestJS Guide
  • Next.js App Router Guide
  • The Complete TypeScript Guide

Keywords in this post

Prisma, ORM, TypeScript, Database, PostgreSQL, MySQL, Backend

Frequently asked questions (FAQ)

Q. How does Prisma compare to TypeORM?

A. Prisma generally offers stronger type safety and a better developer experience. TypeORM exposes more features but tends to be more complex.

Q. Does Prisma support MongoDB?

A. Yes, MongoDB is supported. For relational workloads, PostgreSQL or MySQL are still the most common recommendations.

Q. Can I use raw SQL?

A. Yes. Use prisma.$queryRaw (and related APIs) when you need raw SQL.

Q. Is Prisma production-ready?

A. Yes. Many companies run it reliably in production.