| |

Node.js 45 🟢 Modern Object-Relational Mapping (ORM) with Prisma

Prisma is a next-generation ORM for Node.js and TypeScript that takes a different approach from the traditional ORMs. It is built around a declarative schema file, a generated type-safe client, and an integrated migration system. The schema is the single source of truth: it defines the data models, the relations, and the database connection, and the Prisma Client is generated from it with full TypeScript types . The result is a query API that is validated at compile time, with autocompletion and type inference for every query, including the nested relations.

The recent releases have changed the architecture significantly. Prisma 6.16 removed the Rust query engine and replaced it with a TypeScript-based query compiler, which reduced the bundle size by roughly 90% and made queries up to three times faster . Prisma 7 made the driver adapter mandatory for direct database connections and moved the generated client out of node_modules into the project source code . The current release, Prisma 8, is a ground-up TypeScript rewrite that adds a SQL query builder, TypeScript schema authoring, and extensibility through model collections and middleware .

This chapter covers three areas. First, why Prisma exists — the type-safety problem, the schema-first approach, and the difference from the traditional ORMs. Second, how the schema and the client work — the Prisma Schema Language, the models, the relations, the migration workflow, and the generated client. Third, the CRUD operations and the patterns — the create, findMany, update, delete, the nested writes, the transactions, and the error handling. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the schema and client flow.

Key point: Prisma is a schema-first ORM for Node.js and TypeScript. The Prisma schema defines the data models and the database connection, and the Prisma Client is generated from it with full type safety. The client provides a fluent query API for the CRUD operations, the nested writes, and the transactions. Prisma Migrate generates and applies the SQL migrations from the schema. The current release is Prisma 8, which includes a SQL query builder and TypeScript schema authoring .


Why Prisma exists

The type-safety problem. The traditional database tools force a trade-off between productivity and control. The raw SQL gives full control but lacks the type safety and is error-prone. The traditional ORMs improve the productivity but abstract too much, leading to the impedance mismatch and the performance pitfalls like the n+1 problem. Prisma takes a different approach: the queries are validated at compile time, and the result types are inferred from the schema. A typo in a column name is a compile error, not a runtime error .

The schema-first problem. In Prisma, the schema is the single source of truth. The data models, the relations, and the database connection are all defined in one file, and the client and the migrations are generated from it. The schema is the contract, and the code is derived from the contract. The alternative — the code-first approach where the schema is derived from the models — is the approach of TypeORM and Sequelize. The schema-first approach makes the data model explicit and the migrations deterministic .

The migration problem. Prisma Migrate generates the SQL migrations from the schema and applies them to the database. The workflow is: modify the schema, run prisma migrate dev to generate and apply the migration in development, and run prisma migrate deploy to apply the pending migrations in production. The migration history is stored in the prisma/migrations folder, and the _prisma_migrations table tracks the applied migrations . The migration files are the version control for the database schema, and they are committed with the application code.

The client-generation problem. The Prisma Client is generated from the schema, and it exposes a query API tailored to the models. The client is type-safe: the queries are validated at compile time, and the results are typed. The generation is a build step, and the client must be regenerated whenever the schema changes. In Prisma 7 and later, the generated client is placed in the project source code instead of node_modules, so the dev tools and the file watchers see it as part of the application .

The modern-architecture problem. Prisma 6.16 removed the Rust query engine and replaced it with a TypeScript query compiler. The Rust engine was fast, but it limited the contributions (the contributors needed the Rust experience) and the communication layer between the Rust and the JavaScript runtime was slow. The TypeScript compiler removes the binary dependency, reduces the bundle size, and improves the query performance . Prisma 7 made the driver adapter mandatory, which standardizes the database connectivity across the Node.js, the serverless, and the edge environments .

The trade-off. Prisma is a full ORM, and it abstracts the SQL. The complex queries may need the raw SQL escape hatch, and the generated SQL must be inspected for the performance tuning. The schema-first approach requires the schema to be the source of truth, which is a discipline. The trade-off is between the productivity and the type safety of the ORM and the directness and the control of the raw SQL. For the server-side applications with the stable data models, Prisma is a strong choice .


a. The Prisma schema

The Prisma schema is a file named schema.prisma that contains three parts: the data source, the generator, and the data model.

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
}

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

enum Role {
  USER
  ADMIN
}

The datasource block specifies the database provider and the connection URL. The generator block specifies the client generator and the output path. The data model is the collection of the models, and each model maps to a table in the relational database or a collection in MongoDB .

The models. A model is defined with the model keyword, and it has the fields. The @id attribute marks the primary key. The @default attribute provides the default value. The @unique attribute creates the unique index. The @updatedAt attribute updates the field on every write .

The relations. A relation connects two models. The @relation attribute specifies the foreign key and the referenced field. A one-to-many relation is defined by the list field on the one side (posts Post[]) and the relation field on the many side (author User @relation(...)). The fields argument specifies the foreign key column, and the references argument specifies the referenced column .

The scalar types. Prisma supports String, Int, BigInt, Float, Decimal, Boolean, DateTime, Bytes, Json, Jsonb, and the database-specific types. The Decimal type is stored as a string in the client to avoid the precision loss, and the Bytes type is a Uint8Array .


b. The Prisma Client

The Prisma Client is generated from the schema with prisma generate. The client is instantiated once and reused throughout the application.

import { PrismaClient } from './generated/prisma';
import { PrismaPg } from '@prisma/adapter-pg';

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });

export default prisma;

In Prisma 7 and later, the driver adapter is mandatory for the direct database connections. The @prisma/adapter-pg package provides the PostgreSQL adapter, and the equivalent packages exist for the other databases . The client should be instantiated once per application, because each instance opens its own connection pool. In the development with the hot reload, the globalThis pattern prevents the duplicate instances .

const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };

export const prisma = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;

The CRUD operations. The client provides the methods for the create, the read, the update, and the delete.

// Create
const user = await prisma.user.create({
  data: { email: 'alice@example.com', name: 'Alice' },
});

// Read all
const users = await prisma.user.findMany();

// Read by unique field
const user = await prisma.user.findUnique({ where: { email: 'alice@example.com' } });

// Read with a filter
const admins = await prisma.user.findMany({ where: { role: 'ADMIN' } });

// Update
const updated = await prisma.user.update({
  where: { id: 1 },
  data: { name: 'Alice Smith' },
});

// Delete
await prisma.user.delete({ where: { id: 1 } });

The create method inserts a row and returns the created record. The findMany returns the array of the records, and the findUnique returns a single record by the unique field. The update modifies the record, and the delete removes it .

The select and the include. The select argument limits the returned fields, and the include argument fetches the related records.

// Select specific fields
const users = await prisma.user.findMany({
  select: { id: true, name: true, email: true },
});

// Include related records
const usersWithPosts = await prisma.user.findMany({
  include: { posts: true },
});

The select is the performance optimization: it limits the columns that are returned. The include fetches the relations, and in Prisma 5 and later, it uses a single JOIN for the one-to-many relations .

The nested writes. The create and the update methods support the nested writes, which create or update the related records in the same query.

const user = await prisma.user.create({
  data: {
    email: 'alice@example.com',
    name: 'Alice',
    posts: {
      create: [
        { title: 'First post' },
        { title: 'Second post' },
      ],
    },
  },
  include: { posts: true },
});

The create inside the posts creates the related posts and links them to the user. The connect option links to an existing record, and the create option creates a new one .


c. The migrations and the lifecycle

The migration workflow. The migration workflow is: modify the schema, run prisma migrate dev to generate and apply the migration in development, and run prisma migrate deploy to apply the pending migrations in production .

# Development: generate and apply
npx prisma migrate dev --name add_user_role

# Production: apply the pending migrations
npx prisma migrate deploy

The migrate dev command generates the SQL migration file from the schema change, applies it to the development database, and updates the _prisma_migrations table. The migrate deploy command applies the pending migrations to the production database and does not reset the database or generate the artifacts .

The seeding. The seed script populates the database with the initial data. It is registered in the prisma.config.ts file and run with prisma db seed .

// prisma/seed.ts
import prisma from '../src/prisma';

async function main() {
  await prisma.user.deleteMany();
  await prisma.user.create({
    data: { email: 'alice@example.com', name: 'Alice' },
  });
}

main().catch((e) => console.error(e)).finally(() => prisma.$disconnect());

The transactions. The $transaction method runs a series of queries in a transaction. The interactive form takes a callback that receives the transaction client, and the queries inside the callback run on the same connection .

const result = await prisma.$transaction(async (tx) => {
  const user = await tx.user.findUniqueOrThrow({ where: { id: 1 } });
  if (user.role !== 'ADMIN') throw new Error('Forbidden');
  return tx.post.create({ data: { title: 'New post', authorId: user.id } });
});

The transaction commits when the callback resolves, and it rolls back when the callback throws. The tx client must be used inside the callback, not the outer prisma client .

The error handling. The Prisma errors are the instances of PrismaClientKnownRequestError, PrismaClientValidationError, and the others. The known request error has a code that identifies the error type.

import { Prisma } from './generated/prisma';

try {
  await prisma.user.create({ data: { email: 'alice@example.com' } });
} catch (e) {
  if (e instanceof Prisma.PrismaClientKnownRequestError) {
    if (e.code === 'P2002') throw new ConflictError('Email already exists');
    if (e.code === 'P2025') throw new NotFoundError('Record not found');
  }
  throw e;
}

The P2002 code is the unique constraint violation, and the P2025 is the record not found. The error handling is the pattern for the API responses .

The soft delete. The soft delete is the pattern where the records are marked as deleted instead of being removed. The queries must filter the deleted records explicitly.

const activeUsers = await prisma.user.findMany({ where: { deletedAt: null } });
await prisma.user.update({ where: { id: 1 }, data: { deletedAt: new Date() } });

The explicit filter is the recommended approach over the middleware, because the middleware hides the behavior and is hard to debug .


Complete Example Session

// ============================================
// PART 1: THE SCHEMA
// ============================================

// prisma/schema.prisma
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client"
  output   = "../src/generated/prisma"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  posts     Post[]
  createdAt DateTime @default(now())
}

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

enum Role {
  USER
  ADMIN
}


// ============================================
// PART 2: THE CLIENT
// ============================================

// src/prisma.js
import { PrismaClient } from './generated/prisma';
import { PrismaPg } from '@prisma/adapter-pg';

const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL });
const prisma = new PrismaClient({ adapter });

export default prisma;


// ============================================
// PART 3: THE CREATE
// ============================================

async function createUser() {
  const user = await prisma.user.create({
    data: { email: 'alice@example.com', name: 'Alice' },
  });
  console.log(user.id); // the generated id
  return user;
}


// ============================================
// PART 4: THE READ
// ============================================

async function findUsers() {
  // All users
  const all = await prisma.user.findMany();

  // By unique field
  const byEmail = await prisma.user.findUnique({
    where: { email: 'alice@example.com' },
  });

  // With a filter
  const admins = await prisma.user.findMany({ where: { role: 'ADMIN' } });

  // With the selected fields
  const names = await prisma.user.findMany({
    select: { id: true, name: true },
  });

  // With the related records
  const withPosts = await prisma.user.findMany({
    include: { posts: true },
  });

  return { all, byEmail, admins, names, withPosts };
}


// ============================================
// PART 5: THE NESTED WRITE
// ============================================

async function createUserWithPosts() {
  const user = await prisma.user.create({
    data: {
      email: 'bob@example.com',
      name: 'Bob',
      posts: {
        create: [
          { title: 'First post' },
          { title: 'Second post' },
        ],
      },
    },
    include: { posts: true },
  });
  return user;
}


// ============================================
// PART 6: THE UPDATE AND THE DELETE
// ============================================

async function updateUser(id, data) {
  return prisma.user.update({ where: { id }, data });
}

async function deleteUser(id) {
  return prisma.user.delete({ where: { id } });
}


// ============================================
// PART 7: THE TRANSACTION
// ============================================

async function createPostForAdmin(userId, title) {
  return prisma.$transaction(async (tx) => {
    const user = await tx.user.findUniqueOrThrow({ where: { id: userId } });
    if (user.role !== 'ADMIN') throw new Error('Forbidden');
    return tx.post.create({ data: { title, authorId: userId } });
  });
}


// ============================================
// PART 8: THE ERROR HANDLING
// ============================================

async function safeCreateUser(email, name) {
  try {
    return await prisma.user.create({ data: { email, name } });
  } catch (e) {
    if (e instanceof Prisma.PrismaClientKnownRequestError) {
      if (e.code === 'P2002') throw new Error('Email already exists');
    }
    throw e;
  }
}


// ============================================
// PART 9: THE CURSOR PAGINATION
// ============================================

async function getPosts(cursor, limit = 20) {
  const items = await prisma.post.findMany({
    where: { published: true },
    orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],
    take: limit + 1,
    ...(cursor && { cursor: { id: cursor }, skip: 1 }),
  });

  const hasNextPage = items.length > limit;
  if (hasNextPage) items.pop();

  return {
    items,
    nextCursor: hasNextPage ? items[items.length - 1].id : null,
  };
}


// ============================================
// PART 10: THE COMPLETE APPLICATION
// ============================================

import express from 'express';
const app = express();
app.use(express.json());

app.get('/users', async (req, res) => {
  const users = await prisma.user.findMany({
    select: { id: true, email: true, name: true, role: true },
  });
  res.json(users);
});

app.get('/users/:id', async (req, res) => {
  const user = await prisma.user.findUnique({
    where: { id: parseInt(req.params.id, 10) },
  });
  if (!user) return res.status(404).json({ error: 'User not found' });
  res.json(user);
});

app.post('/users', async (req, res) => {
  try {
    const user = await prisma.user.create({ data: req.body });
    res.status(201).json(user);
  } catch (e) {
    if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
      return res.status(409).json({ error: 'Email already exists' });
    }
    res.status(500).json({ error: 'Internal server error' });
  }
});

app.patch('/users/:id', async (req, res) => {
  const user = await prisma.user.update({
    where: { id: parseInt(req.params.id, 10) },
    data: req.body,
  });
  res.json(user);
});

app.delete('/users/:id', async (req, res) => {
  await prisma.user.delete({ where: { id: parseInt(req.params.id, 10) } });
  res.status(204).send();
});

app.listen(3000);

The ten parts show the schema, the client, the create, the read, the nested write, the update and delete, the transaction, the error handling, the cursor pagination, and the complete application.


Quick Reference

The Schema Blocks

BlockPurpose
datasourceThe database provider and the URL
generatorThe client generator and the output
modelThe data models
enumThe enumerated types

The Model Attributes

AttributePurpose
@idThe primary key
@defaultThe default value
@uniqueThe unique index
@relationThe foreign key relation
@updatedAtThe update timestamp
@mapThe database column name
@@mapThe database table name

The CRUD Methods

MethodPurpose
create({ data })Insert a record
findMany({ where, select, include })Query multiple
findUnique({ where })Query by unique field
findFirst({ where })Query the first match
update({ where, data })Modify a record
delete({ where })Remove a record
upsert({ where, create, update })Update or create

The Migration Commands

CommandPurpose
prisma migrate devGenerate and apply in development
prisma migrate deployApply the pending in production
prisma migrate resetDrop, create, migrate, seed
prisma db seedRun the seed script
prisma generateRegenerate the client
prisma studioOpen the data browser

The Error Codes

CodeMeaning
P2002The unique constraint violation
P2025The record not found
P2003The foreign key constraint violation

Best Practices

✅ Do This:

// Use the singleton pattern for the client
const prisma = globalForPrisma.prisma ?? new PrismaClient();              // ✅
// Use the select for the performance
prisma.user.findMany({ select: { id: true, name: true } });               // ✅
// Use the include for the relations
prisma.user.findMany({ include: { posts: true } });                       // ✅
// Use the transactions for the multi-step operations
prisma.$transaction(async (tx) => { ... });                               // ✅
// Use the cursor pagination for the large datasets
prisma.post.findMany({ take: limit + 1, cursor: { id: cursor }, skip: 1 }); // ✅
// Handle the P2002 errors
if (e.code === 'P2002') throw new ConflictError();                        // ✅
// Use the schema-first workflow
// Modify the schema and run prisma migrate dev                           // ✅

❌ Don’t Do This:

// Don't create a new PrismaClient per request
const prisma = new PrismaClient(); // per request                          // ❌
// Don't use the outer client in a transaction
prisma.$transaction(async (tx) => { await prisma.user.create(...); });    // ❌
// Don't use the offset pagination for the feeds
prisma.post.findMany({ skip: page * 20, take: 20 }); // unstable          // ❌
// Don't forget the error handling
await prisma.user.create({ data }); // no try/catch                       // ❌
// Don't use the raw SQL without the Prisma.sql
prisma.$queryRawUnsafe(`SELECT * FROM users WHERE id = ${id}`);           // ❌
// Don't forget to regenerate the client after the schema change
// prisma generate                                                       // ❌

Common Pitfalls

PitfallWhy It HappensFix
Connection exhaustionA new client per requestUse the singleton
The n+1 problemThe relations loaded in a loopUse include
The unique constraint errorNo P2002 handlingAdd the error handling
The transaction does not use txThe outer client is usedUse the tx client
The generated client is staleNo prisma generateRegenerate
The migration driftA manual schema changeUse migrate dev
The unstable paginationThe offset paginationUse the cursor pagination

Real-World Examples

1. The Client Singleton

const globalForPrisma = globalThis;
export const prisma = globalForPrisma.prisma ?? new PrismaClient();

2. The Create

const user = await prisma.user.create({ data: { email: 'alice@example.com' } });

3. The Find Many

const users = await prisma.user.findMany({ where: { role: 'ADMIN' } });

4. The Find Unique

const user = await prisma.user.findUnique({ where: { email: 'alice@example.com' } });

5. The Select

const names = await prisma.user.findMany({ select: { id: true, name: true } });

6. The Include

const withPosts = await prisma.user.findMany({ include: { posts: true } });

7. The Nested Write

prisma.user.create({ data: { email, posts: { create: [{ title }] } } });

8. The Transaction

prisma.$transaction(async (tx) => { ... });

9. The Cursor Pagination

prisma.post.findMany({ take: 21, cursor: { id: cursor }, skip: 1 });

10. The Error Handling

if (e.code === 'P2002') throw new ConflictError();

Visual

The Schema and Client Flow

┌──────────────────────────────────────────────────────────────┐
│  THE SCHEMA AND CLIENT FLOW                                  │
│                                                              │
│  prisma/schema.prisma                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  datasource db { ... }                               │    │
│  │  generator client { ... }                            │    │
│  │  model User { ... }                                  │    │
│  │  model Post { ... }                                  │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  prisma generate                                        │
│    ▼                                                         │
│  src/generated/prisma/                                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The type-safe client                                │    │
│  │  ├── user.create()                                   │    │
│  │  ├── user.findMany()                                 │    │
│  │  ├── user.findUnique()                               │    │
│  │  ├── user.update()                                   │    │
│  │  └── user.delete()                                   │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    │  prisma migrate dev                                     │
│    ▼                                                         │
│  prisma/migrations/                                          │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  20240101_init/migration.sql                         │    │
│  │  20240102_add_role/migration.sql                     │    │
│  └──────────────────────────────────────────────────────┘    │
│    │                                                         │
│    ▼                                                         │
│  The database                                                │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The CRUD Operations

┌──────────────────────────────────────────────────────────────┐
│  THE CRUD OPERATIONS                                         │
│                                                              │
│  CREATE:                                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  prisma.user.create({ data: { email, name } })       │    │
│  │  prisma.user.createMany({ data: [...] })             │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  READ:                                                       │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  prisma.user.findMany({ where, select, include })    │    │
│  │  prisma.user.findUnique({ where: { email } })        │    │
│  │  prisma.user.findFirst({ where })                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  UPDATE:                                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  prisma.user.update({ where: { id }, data })         │    │
│  │  prisma.user.updateMany({ where, data })             │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  DELETE:                                                     │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  prisma.user.delete({ where: { id } })               │    │
│  │  prisma.user.deleteMany({ where })                   │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Transaction

┌──────────────────────────────────────────────────────────────┐
│  THE TRANSACTION                                             │
│                                                              │
│  const result = await prisma.$transaction(async (tx) => {    │
│    │                                                         │
│    │  The tx client is scoped to the transaction.            │
│    │                                                         │
│    ├── const user = await tx.user.findUniqueOrThrow(...)     │
│    │                                                         │
│    ├── if (user.role !== 'ADMIN') throw new Error()          │
│    │                                                         │
│    └── return tx.post.create({ data: { ... } })              │
│  });                                                         │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The callback resolves → COMMIT                      │    │
│  │  The callback throws   → ROLLBACK                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The tx client MUST be used inside the callback.             │
│  The outer prisma client bypasses the transaction.           │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Cursor Pagination

┌──────────────────────────────────────────────────────────────┐
│  THE CURSOR PAGINATION                                       │
│                                                              │
│  const items = await prisma.post.findMany({                  │
│    where: { published: true },                               │
│    orderBy: [{ createdAt: 'desc' }, { id: 'desc' }],         │
│    take: limit + 1,                                          │
│    ...(cursor && { cursor: { id: cursor }, skip: 1 }),       │
│  });                                                         │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  1. Fetch limit + 1 items.                           │    │
│  │  2. If items.length > limit, there is a next page.   │    │
│  │  3. Pop the extra item.                              │    │
│  │  4. Return the next cursor (the last item's id).     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  The id is the secondary sort to prevent the         │    │
│  │  unstable pagination when the timestamps are equal.  │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
PrismaA schema-first ORM for Node.js and TypeScript
Prisma schemaThe data source, the generator, and the data model
Prisma ClientThe generated type-safe query API
Prisma MigrateThe migration system
Prisma StudioThe data browser
Current releasePrisma 8
The driver adapterMandatory in Prisma 7+
The client singletonThe pattern for the connection pool
The nested writesThe create and connect inside the create/update
The $transactionThe transaction wrapper
The error codesP2002, P2025, P2003

Key takeaways:

  • Prisma is a schema-first ORM for Node.js and TypeScript. The schema defines the data models and the database connection, and the client and the migrations are generated from it. The queries are validated at compile time, and the result types are inferred from the schema .
  • The Prisma schema is the single source of truth. It contains the data source, the generator, and the data model. The models map to the tables, and the relations connect them. The @id, @default, @unique, and @relation attributes define the structure .
  • The Prisma Client is generated from the schema. The client is type-safe, and the query API is tailored to the models. The client should be instantiated once per application, and the globalThis pattern prevents the duplicate instances in the development .
  • Prisma 6.16 removed the Rust query engine. The TypeScript query compiler replaced it, which reduced the bundle size by 90% and made the queries up to three times faster. Prisma 7 made the driver adapter mandatory and moved the generated client out of node_modules .
  • The CRUD operations are the create, findMany, findUnique, update, and delete. The select limits the returned fields, and the include fetches the relations. The nested writes create and link the related records in the same query .
  • The migration workflow is migrate dev in development and migrate deploy in production. The migrate dev generates and applies the migration, and the migrate deploy applies the pending migrations. The migration history is in the prisma/migrations folder .
  • The transactions are scoped to the $transaction callback. The tx client must be used inside the callback, and the outer prisma client bypasses the transaction. The transaction commits when the callback resolves and rolls back when it throws .
  • The error handling uses the PrismaClientKnownRequestError. The P2002 code is the unique constraint violation, the P2025 is the record not found, and the P2003 is the foreign key violation .

Remember: Prisma is the schema-first ORM that brings the type safety and the productivity to the database access in Node.js and TypeScript. The schema is the single source of truth, and the client and the migrations are generated from it. The recent releases removed the Rust engine, made the driver adapter mandatory, and moved the generated client into the project source. The CRUD operations are type-safe, the nested writes create and link the related records, and the transactions scope the multi-step operations. The cursor pagination is the pattern for the large datasets, and the error handling uses the known request codes. The result is a data access layer that is type-safe, productive, and modern.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!