Client Extensions
Prisma Intermediate/Advanced
Chapter 6 ยท Client Extensions
Some behaviour belongs everywhere: logging slow queries, hiding soft-deleted posts, adding a computed
url field. Copying that into every route invites mistakes. Client extensions let you
build it into Prisma Client itself, with $extends.
prisma.$use(...) middleware. It was deprecated in Prisma 5 and removed in Prisma 7.
The query component of $extends, covered below, replaces it.
How $extends Works
$extends returns a new client and leaves the original untouched. You can chain several
extensions — base.$extends(a).$extends(b) — and if two define the same thing, the later
one wins. An extension object can have four parts:
| Component | Adds or changes | Example |
|---|---|---|
| query | What happens when a query runs | Timing, filtering, default arguments |
| model | New methods on a model | prisma.post.findPublishedBySlug() |
| result | Computed fields on returned records | post.url |
| client | New top-level methods | prisma.$healthCheck() |
query: Wrapping Every Query
query(args)runs the original operation. Forget to call it, or to return its result, and the query silently does nothing useful.- Target one model and operation (
post: { findMany(...) }), all operations on a model (post: { $allOperations(...) }), or everything ($allModels). - Raw queries have their own top-level hooks:
$queryRaw,$executeRawand theUnsafevariants.
Changing arguments: a soft-delete filter
Soft deletion marks a row as deleted instead of removing it, so it can be restored. Add
deletedAt DateTime? to Post and migrate, then hide deleted posts by default:
- Nested reads aren't covered. Prisma's docs state that query extensions don't apply to nested read and write operations.
prisma.user.findMany({ include: { posts: true } })still returns deleted posts. - Every operation needs thought.
findUnique,aggregate,groupBy, raw SQL and updates each behave differently; the example above covers only three. - You can't change
selectorincludein a query extension, because that would change the result's type.
model: Adding Methods
Model methods keep business rules in one place: routes call findPublishedBySlug instead of
remembering all three conditions. The $allModels version uses
Prisma.getExtensionContext(this) to find out which model it was called on.
result: Computed Fields
needs lists the real fields the computation uses, so Prisma fetches them even when you only
select the computed field. Computed fields are typed, can be selected like real ones, and are
calculated only when read. They can't be used in where or orderBy — the database
doesn't know they exist.
client: Top-Level Methods
Packaging and Sharing Extensions
PrismaClient. Functions that receive the client (as in
Chapter 5) should use AppPrisma; otherwise TypeScript won't know about softDelete or
url. In unit tests, you can still mock it with mockDeep<AppPrisma>().
Hands-On Exercises
Write a query extension that makes post.findMany return at most 50 rows unless the caller passes take, and never more than 100 even if they do.
Add a result extension giving users a displayName (their name, or the part of their email before the @ if they have none), and a model method user.findByEmail that ignores letter case in the email.
Using the soft-delete extension from this chapter, write an integration test (Chapter 5 style) that shows exactly which queries hide a soft-deleted post and which still return it. Explain what you'd change before relying on it.
๐ View solutionChapter 6 Quick Reference
client.$extends({...})returns a new client; the original is unchanged; chain several; later winsquery: wrap operations; alwaysreturn query(args);$allModels/$allOperations; replaces removed$usemiddleware- Query extensions don't apply to nested reads/writes and can't change
select/include model: custom methods;Prisma.getExtensionContext(this)in$allModelsresult: computed fields withneedsandcompute; not usable inwhere/orderByclient: top-level$methods; package reusable ones withPrisma.defineExtension- Export
type AppPrisma = typeof prismaand use it wherever the client is passed around