Claude Code for Prisma Extensions: Custom Queries and Middleware — Claude Skills 360 Blog
Blog / Backend / Claude Code for Prisma Extensions: Custom Queries and Middleware
Backend

Claude Code for Prisma Extensions: Custom Queries and Middleware

Published: March 26, 2027
Read time: 8 min read
By: Claude Skills 360

Prisma Extensions add custom behavior to Prisma Client without modifying the schema — prisma.$extends({ model, query, result, client }) extends the client. The model component adds custom static methods: prisma.user.findManyActive(). The query component intercepts operations: query.user.findMany({ args, query }) can inject where: { deletedAt: null } for soft deletes. The result component adds computed fields to returned data without a database column. The client component adds top-level methods. Multiple $extends calls compose extensions, so you can stack soft-delete, multi-tenancy, and pagination extensions. Prisma.defineExtension creates shareable extension factories. Claude Code generates Prisma Extension factories for soft delete, multi-tenancy where-clause injection, computed fields, audit trail logging, and custom pagination helpers.

CLAUDE.md for Prisma Extensions

## Prisma Extensions Stack
- Version: prisma >= 5.14, @prisma/client >= 5.14
- Extend: const xprisma = prisma.$extends({ model, query, result, client })
- Soft delete: query component intercepts delete/findMany — injects deletedAt filter
- Computed: result component adds { name: { needs: { firstName }, compute: d => d.firstName } }
- Middleware: query.{ model }.{ operation } = async ({ args, query }) => { mutate args; return query(args) }
- Factory: Prisma.defineExtension — reusable named extension
- Stack: const db = prisma.$extends(softDelete).$extends(tenancy).$extends(pagination)

Soft Delete Extension

// lib/prisma/extensions/soft-delete.ts
import { Prisma } from "@prisma/client"

// Models that support soft delete — must have deletedAt: DateTime? field
type SoftDeletableModel = "order" | "customer" | "product"
const SOFT_DELETABLE: SoftDeletableModel[] = ["order", "customer", "product"]

export const softDeleteExtension = Prisma.defineExtension({
  name: "soft-delete",
  query: {
    $allModels: {
      // Intercept delete — set deletedAt instead of hard deleting
      async delete({ model, args, query }) {
        if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
          return (query as any)({
            ...args,
            data: { deletedAt: new Date() },
          } as Parameters<typeof query>[0])
        }
        return query(args)
      },

      // Intercept deleteMany
      async deleteMany({ model, args, query }) {
        if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
          return (query as any)({
            ...args,
            data: { deletedAt: new Date() },
          } as Parameters<typeof query>[0])
        }
        return query(args)
      },

      // Auto-exclude soft-deleted records from all reads
      async findMany({ model, args, query }) {
        if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
          args.where = { ...args.where, deletedAt: null }
        }
        return query(args)
      },

      async findFirst({ model, args, query }) {
        if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
          args.where = { ...args.where, deletedAt: null }
        }
        return query(args)
      },

      async findUnique({ model, args, query }) {
        if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
          // Convert to findFirst to allow adding where clause
          return query(args)
        }
        return query(args)
      },

      async count({ model, args, query }) {
        if (SOFT_DELETABLE.includes(model.toLowerCase() as SoftDeletableModel)) {
          args.where = { ...args.where, deletedAt: null }
        }
        return query(args)
      },
    },
  },
  model: {
    $allModels: {
      // Hard delete for admin operations — bypasses soft delete
      async hardDelete<T>(
        this: T,
        where: Parameters<Prisma.PrismaClientKnownRequestError["constructor"]>[0]
      ) {
        const context = Prisma.getExtensionContext(this)
        return (context as any).delete({ where })
      },

      // Restore a soft-deleted record
      async restore<T>(this: T, where: object) {
        const context = Prisma.getExtensionContext(this)
        return (context as any).update({ where, data: { deletedAt: null } })
      },

      // Find including deleted
      async findWithDeleted<T>(this: T, args: object = {}) {
        const context = Prisma.getExtensionContext(this)
        return (context as any).findMany(args)
      },
    },
  },
})

Multi-Tenancy Extension

// lib/prisma/extensions/tenancy.ts — row-level tenant isolation
import { Prisma } from "@prisma/client"
import { AsyncLocalStorage } from "async_hooks"

// Tenant context storage — set this per-request
const tenantStorage = new AsyncLocalStorage<{ tenantId: string }>()

export function withTenant<T>(tenantId: string, fn: () => Promise<T>): Promise<T> {
  return tenantStorage.run({ tenantId }, fn)
}

// Models with tenantId column
const TENANT_SCOPED = ["order", "customer", "product", "invoice"]

export const tenancyExtension = Prisma.defineExtension({
  name: "tenancy",
  query: {
    $allModels: {
      async $allOperations({ model, operation, args, query }) {
        const store = tenantStorage.getStore()
        if (!store) return query(args)  // No tenant context — skip (e.g., background jobs)

        if (!TENANT_SCOPED.includes(model.toLowerCase())) return query(args)

        const { tenantId } = store
        const mutatingOps = ["create", "createMany", "upsert"]
        const readOps = ["findMany", "findFirst", "findUnique", "count", "aggregate", "groupBy"]
        const writeOps = ["update", "updateMany", "delete", "deleteMany"]

        if (mutatingOps.includes(operation)) {
          // Inject tenantId on create
          if (args.data) {
            args = { ...args, data: { ...args.data, tenantId } }
          }
        } else if (readOps.includes(operation) || writeOps.includes(operation)) {
          // Inject where clause for reads and updates
          args = { ...args, where: { ...args.where, tenantId } }
        }

        return query(args)
      },
    },
  },
})

Computed Fields Extension

// lib/prisma/extensions/computed-fields.ts — result transformations
import { Prisma } from "@prisma/client"

export const computedFieldsExtension = Prisma.defineExtension({
  name: "computed-fields",
  result: {
    // Add fullName computed field to User model
    user: {
      fullName: {
        needs: { firstName: true, lastName: true },
        compute(user) {
          return `${user.firstName} ${user.lastName}`.trim()
        },
      },
      initials: {
        needs: { firstName: true, lastName: true },
        compute(user) {
          const f = user.firstName?.[0] ?? ""
          const l = user.lastName?.[0] ?? ""
          return `${f}${l}`.toUpperCase()
        },
      },
    },

    // Add formatted total to Order model
    order: {
      totalDollars: {
        needs: { totalCents: true },
        compute(order) {
          return (order.totalCents / 100).toFixed(2)
        },
      },
      isOverdue: {
        needs: { status: true, createdAt: true },
        compute(order) {
          if (order.status === "delivered" || order.status === "cancelled") return false
          const slaHours = 48
          const deadline = new Date(order.createdAt.getTime() + slaHours * 3600_000)
          return new Date() > deadline
        },
      },
    },
  },
})

Pagination Extension

// lib/prisma/extensions/pagination.ts — cursor and offset pagination
import { Prisma } from "@prisma/client"

interface PaginateArgs {
  page?: number
  limit?: number
  cursor?: string
}

export const paginationExtension = Prisma.defineExtension({
  name: "pagination",
  model: {
    $allModels: {
      async paginate<T, A>(
        this: T,
        args: Prisma.Exact<A, Prisma.Args<T, "findMany"> & PaginateArgs>
      ) {
        const ctx = Prisma.getExtensionContext(this)
        const { page = 1, limit = 20, cursor, ...findArgs } = args as any

        if (cursor) {
          // Cursor-based pagination (for infinite scroll)
          const items = await (ctx as any).findMany({
            ...findArgs,
            take: limit + 1,
            cursor: { id: cursor },
            skip: 1,
          })
          const hasMore = items.length > limit
          return {
            items: hasMore ? items.slice(0, limit) : items,
            nextCursor: hasMore ? items[limit - 1]?.id : null,
            hasMore,
          }
        }

        // Offset-based pagination
        const skip = (page - 1) * limit
        const [items, total] = await Promise.all([
          (ctx as any).findMany({ ...findArgs, take: limit, skip }),
          (ctx as any).count({ where: findArgs.where }),
        ])

        return {
          items,
          total,
          page,
          totalPages: Math.ceil(total / limit),
          hasMore: skip + items.length < total,
        }
      },
    },
  },
})

Composing Extensions

// lib/prisma/client.ts — stack all extensions
import { PrismaClient } from "@prisma/client"
import { softDeleteExtension } from "./extensions/soft-delete"
import { tenancyExtension } from "./extensions/tenancy"
import { computedFieldsExtension } from "./extensions/computed-fields"
import { paginationExtension } from "./extensions/pagination"

const prismaBase = new PrismaClient({
  log: process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
})

export const db = prismaBase
  .$extends(softDeleteExtension)
  .$extends(tenancyExtension)
  .$extends(computedFieldsExtension)
  .$extends(paginationExtension)

export type ExtendedPrismaClient = typeof db

// Usage
// const orders = await db.order.paginate({ page: 2, limit: 20, where: { status: "pending" } })
// const users = await db.user.findFirst({ ... })  // Returns .fullName computed field
// withTenant("tenant_123", async () => { await db.order.findMany() })  // Auto-filters by tenant

For the Drizzle ORM alternative when query builders, relations API, and migration tooling are preferred without Prisma’s layer of abstraction — Drizzle queries are closer to raw SQL with better TypeScript inference on complex joins, and migration files are plain SQL, see the Drizzle Advanced guide. For the TypeORM alternative when a large existing codebase uses decorator-based entity classes — TypeORM’s ActiveRecord and DataMapper patterns are more familiar to Django/Rails developers but have weaker TypeScript types than Prisma’s generated client, see the ORM comparison guide. The Claude Skills 360 bundle includes Prisma Extensions skill sets covering soft delete, multi-tenancy, and computed fields. Start with the free tier to try database extension 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