Claude Code for Prisma Advanced: Transactions, Extensions, and Type Safety — Claude Skills 360 Blog
Blog / Backend / Claude Code for Prisma Advanced: Transactions, Extensions, and Type Safety
Backend

Claude Code for Prisma Advanced: Transactions, Extensions, and Type Safety

Published: December 22, 2026
Read time: 9 min read
By: Claude Skills 360

Prisma’s three-layer architecture separates schema definition, query generation, and the database driver. $transaction with interactive transactions lets you read-then-write atomically. Client extensions add domain logic directly to the Prisma client — soft delete, multi-tenancy row filtering, audit logging. Middleware intercepts every query for logging or transformation. Prisma Pulse streams real-time database changes as a typed async iterator. Claude Code generates Prisma schema files, client extensions, transaction patterns, and the middleware chains for production Node.js applications.

CLAUDE.md for Prisma Projects

## Prisma Stack
- Version: prisma >= 5.0 with prisma-client-js
- Database: PostgreSQL (primary), SQLite (tests)
- Extensions: $extends for soft delete, tenant isolation, audit logging
- Transactions: $transaction for atomic multi-table operations
- Testing: jest-mock-extended or manual mock via DI
- Migrations: prisma migrate dev (local), prisma migrate deploy (CI/CD)
- Seeding: prisma/seed.ts with @faker-js/faker for realistic data
- Client: singleton pattern in lib/db.ts (prevent hot-reload connections)

Schema with Advanced Features

// prisma/schema.prisma
generator client {
  provider        = "prisma-client-js"
  previewFeatures = ["driverAdapters", "relationJoins"]
}

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

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  role      Role     @default(USER)
  tenantId  String
  tenant    Tenant   @relation(fields: [tenantId], references: [id])
  orders    Order[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
  deletedAt DateTime?  // Soft delete

  @@index([tenantId])
  @@index([email, tenantId])
  @@map("users")
}

model Order {
  id          String      @id @default(cuid())
  status      OrderStatus @default(PENDING)
  totalCents  Int
  userId      String
  user        User        @relation(fields: [userId], references: [id])
  tenantId    String
  items       OrderItem[]
  auditLogs   AuditLog[]
  createdAt   DateTime    @default(now())
  updatedAt   DateTime    @updatedAt
  deletedAt   DateTime?

  @@index([userId, createdAt])
  @@index([tenantId, status])
  @@map("orders")
}

model AuditLog {
  id        String   @id @default(cuid())
  orderId   String
  order     Order    @relation(fields: [orderId], references: [id])
  action    String
  before    Json?
  after     Json?
  userId    String
  createdAt DateTime @default(now())

  @@index([orderId])
  @@map("audit_logs")
}

enum OrderStatus {
  PENDING
  PROCESSING
  SHIPPED
  DELIVERED
  CANCELLED
}

enum Role {
  USER
  ADMIN
  SUPER_ADMIN
}

Singleton Client with Extensions

// lib/db.ts — Prisma client singleton with extensions
import { PrismaClient, Prisma } from '@prisma/client'

function createPrismaClient() {
  return new PrismaClient({
    log: process.env.NODE_ENV === 'development'
      ? ['query', 'error', 'warn']
      : ['error'],
  })
}

// Prevent multiple connections in Next.js hot reload
const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

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

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

// Extended client with domain capabilities
export const db = prismaBase.$extends({
  name: 'soft-delete',
  model: {
    $allModels: {
      // Soft delete: sets deletedAt instead of DELETE
      async softDelete<T>(this: T, where: Prisma.Args<T, 'delete'>['where']) {
        const context = Prisma.getExtensionContext(this)
        return (context as any).update({
          where,
          data: { deletedAt: new Date() },
        })
      },

      // Restore soft-deleted record
      async restore<T>(this: T, where: Prisma.Args<T, 'update'>['where']) {
        const context = Prisma.getExtensionContext(this)
        return (context as any).update({
          where,
          data: { deletedAt: null },
        })
      },
    },
  },
  query: {
    $allModels: {
      // Automatically exclude soft-deleted records
      findMany({ model, operation, args, query }) {
        args.where = { ...args.where, deletedAt: null }
        return query(args)
      },
      findFirst({ args, query }) {
        args.where = { ...args.where, deletedAt: null }
        return query(args)
      },
      findUnique({ args, query }) {
        return query(args)  // findUnique uses ID — don't filter
      },
      count({ args, query }) {
        args.where = { ...args.where, deletedAt: null }
        return query(args)
      },
    },
  },
})

Multi-Tenant Client Extension

// lib/tenant-db.ts — scoped client per tenant
import { db } from './db'

export function createTenantClient(tenantId: string) {
  return db.$extends({
    name: 'tenant-isolation',
    query: {
      $allModels: {
        async findMany({ args, query }) {
          args.where = { ...args.where, tenantId }
          return query(args)
        },
        async findFirst({ args, query }) {
          args.where = { ...args.where, tenantId }
          return query(args)
        },
        async create({ args, query }) {
          if ('data' in args) {
            args.data = { ...args.data, tenantId }
          }
          return query(args)
        },
        async createMany({ args, query }) {
          if (Array.isArray(args.data)) {
            args.data = args.data.map(d => ({ ...d, tenantId }))
          }
          return query(args)
        },
        async update({ args, query }) {
          args.where = { ...args.where, tenantId }
          return query(args)
        },
        async delete({ args, query }) {
          args.where = { ...args.where, tenantId }
          return query(args)
        },
      },
    },
  })
}

// Usage in API route — tenant automatically enforced
// const tenantDb = createTenantClient(req.user.tenantId)
// const orders = await tenantDb.order.findMany()  // WHERE tenantId = ?

Interactive Transactions

// lib/order-service.ts
import { db } from './db'
import { Prisma, OrderStatus } from '@prisma/client'

export async function createOrderWithInventory(input: {
  userId: string
  tenantId: string
  items: Array<{ productId: string; quantity: number; priceCents: number }>
}) {
  // Interactive transaction: read-then-write atomically
  return db.$transaction(async (tx) => {
    // 1. Verify all products exist and have sufficient stock
    for (const item of input.items) {
      const product = await tx.product.findUnique({
        where: { id: item.productId },
        select: { id: true, stockQuantity: true, name: true },
      })

      if (!product) {
        throw new Error(`Product ${item.productId} not found`)
      }

      if (product.stockQuantity < item.quantity) {
        throw new Error(`Insufficient stock for ${product.name}`)
      }
    }

    // 2. Create order
    const totalCents = input.items.reduce(
      (sum, item) => sum + item.priceCents * item.quantity,
      0
    )

    const order = await tx.order.create({
      data: {
        userId: input.userId,
        tenantId: input.tenantId,
        totalCents,
        status: OrderStatus.PENDING,
        items: {
          create: input.items.map(item => ({
            productId: item.productId,
            quantity: item.quantity,
            priceCents: item.priceCents,
          })),
        },
      },
      include: {
        items: true,
        user: { select: { name: true, email: true } },
      },
    })

    // 3. Decrement stock atomically
    for (const item of input.items) {
      await tx.product.update({
        where: { id: item.productId },
        data: { stockQuantity: { decrement: item.quantity } },
      })
    }

    // 4. Create audit log
    await tx.auditLog.create({
      data: {
        orderId: order.id,
        action: 'ORDER_CREATED',
        after: { status: order.status, totalCents },
        userId: input.userId,
      },
    })

    return order
  }, {
    isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
    timeout: 10000,  // 10 second max
  })
}


export async function cancelOrder(orderId: string, userId: string, reason: string) {
  return db.$transaction(async (tx) => {
    const order = await tx.order.findUnique({
      where: { id: orderId },
      include: { items: true },
    })

    if (!order) throw new Error('Order not found')
    if (!['PENDING', 'PROCESSING'].includes(order.status)) {
      throw new Error(`Cannot cancel order with status ${order.status}`)
    }

    const before = { status: order.status }

    // Update order status
    const updated = await tx.order.update({
      where: { id: orderId },
      data: { status: OrderStatus.CANCELLED },
    })

    // Restore inventory
    for (const item of order.items) {
      await tx.product.update({
        where: { id: item.productId },
        data: { stockQuantity: { increment: item.quantity } },
      })
    }

    // Audit log
    await tx.auditLog.create({
      data: {
        orderId,
        action: 'ORDER_CANCELLED',
        before,
        after: { status: OrderStatus.CANCELLED, reason },
        userId,
      },
    })

    return updated
  })
}

Middleware for Logging and Timing

// lib/db-with-middleware.ts
import { prismaBase } from './db'

prismaBase.$use(async (params, next) => {
  const start = Date.now()
  const result = await next(params)
  const duration = Date.now() - start

  if (duration > 100) {
    console.warn('Slow query:', {
      model: params.model,
      action: params.action,
      duration: `${duration}ms`,
      args: JSON.stringify(params.args).slice(0, 200),
    })
  }

  return result
})

// Middleware: log all mutations
prismaBase.$use(async (params, next) => {
  const writeMutations = ['create', 'createMany', 'update', 'updateMany', 'delete', 'deleteMany']

  if (params.model && writeMutations.includes(params.action)) {
    console.log(`[DB] ${params.model}.${params.action}`)
  }

  return next(params)
})

Type-Safe Query Helpers

// lib/order-queries.ts — reusable typed query builders
import { Prisma } from '@prisma/client'
import { db } from './db'

// Utility type: Order with user and item count
export type OrderWithUser = Prisma.OrderGetPayload<{
  include: {
    user: { select: { id: true; name: true; email: true } }
    _count: { select: { items: true } }
  }
}>

// Paginated query helper
export async function findOrdersPaginated(params: {
  tenantId: string
  status?: string
  page?: number
  pageSize?: number
}): Promise<{ data: OrderWithUser[]; total: number; pages: number }> {
  const { tenantId, status, page = 1, pageSize = 20 } = params
  const skip = (page - 1) * pageSize

  const where: Prisma.OrderWhereInput = {
    tenantId,
    ...(status && { status: status as any }),
  }

  const [data, total] = await Promise.all([
    db.order.findMany({
      where,
      include: {
        user: { select: { id: true, name: true, email: true } },
        _count: { select: { items: true } },
      },
      orderBy: { createdAt: 'desc' },
      skip,
      take: pageSize,
    }),
    db.order.count({ where }),
  ])

  return {
    data,
    total,
    pages: Math.ceil(total / pageSize),
  }
}

For the database migrations and schema evolution patterns that Prisma manages, see the database migrations guide for zero-downtime migration strategies. For the Drizzle ORM alternative with a more SQL-like query syntax, the Drizzle ORM guide covers the schema-first approach with type inference. The Claude Skills 360 bundle includes Prisma skill sets covering client extensions, interactive transactions, and multi-tenant patterns. Start with the free tier to try Prisma schema and query generation.

Keep Reading

Backend

Claude Code for Bun: Fast JavaScript Runtime and Toolkit

Build with Bun and Claude Code — Bun.serve for HTTP servers, Bun.file for fast file I/O, Bun.$ for shell commands, Bun.sql for SQLite and PostgreSQL, Bun.build for bundling, bun:test for testing, Bun.hash for hashing, bun.lock for deterministic installs, bun run for package.json scripts, hot reloading with --hot, bun init for project scaffolding, and compatibility with Node.js modules.

6 min read Jun 13, 2027
Backend

Claude Code for Express.js Advanced: Patterns for Production APIs

Advanced Express.js patterns with Claude Code — typed request handlers with RequestHandler generics, async error handling middleware, Zod validation middleware factory, rate limiting with express-rate-limit and Redis store, helmet security middleware, compression, dependency injection with tsyringe, file upload with multer and S3, pagination utilities, JWT middleware, and structured logging with pino.

6 min read Jun 8, 2027
Backend

Claude Code for KeystoneJS: Node.js CMS and App Framework

Build full-stack apps with KeystoneJS and Claude Code — config with lists, fields.text and fields.relationship for schema definition, access control with isAuthenticated and isAdmin functions, hooks with beforeOperation and afterOperation, GraphQL API auto-generation from schema, AdminUI for content management, session with statelessSessions, Prisma adapter for database, file storage with images and files fields, and custom REST endpoints.

6 min read Jun 7, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free