PrismaSeniorTechnical

Что такое middleware Prisma (устаревший) и расширения Prisma Client (Prisma Client extensions)? Что такое $extends?

$use (deprecated) перехватывал все запросы через middleware-цепочку. $extends (актуальный, Prisma 4.16+) создаёт новый типобезопасный клиент с расширениями query, result, model и client — поддерживает вычисляемые поля, кастомные методы и перехват операций.

Prisma Middleware ($use) — устаревший подход

До Prisma 4.16 для перехвата запросов использовался prisma.$use() — middleware-функция, получающая params (модель, action, аргументы) и next для вызова следующего обработчика. Начиная с Prisma 5 этот API помечен как deprecated.

// УСТАРЕВШИЙ подход — $use (Prisma 4.x и ниже)
const prisma = new PrismaClient();

prisma.$use(async (params, next) => {
  const before = Date.now();
  const result = await next(params);
  const after = Date.now();
  console.log(`${params.model}.${params.action} took ${after - before}ms`);
  return result;
});

// Soft delete через middleware
prisma.$use(async (params, next) => {
  if (params.model === 'Post' && params.action === 'delete') {
    params.action = 'update';
    params.args.data = { deletedAt: new Date() };
  }
  return next(params);
});

Prisma Client Extensions ($extends) — актуальный подход

$extends — это новый API (GA с Prisma 4.16), который заменяет middleware. Он создаёт новый экземпляр клиента с расширенным поведением и полностью типобезопасен. Поддерживает четыре компонента расширений: model, client, query, result.

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient().$extends({
  // query — перехват запросов (аналог middleware)
  query: {
    post: {
      async delete({ args, query }) {
        // Soft delete вместо физического удаления
        return prisma.post.update({
          where: args.where,
          data: { deletedAt: new Date() },
        });
      },
    },
  },

  // result — вычисляемые поля на уровне типов
  result: {
    user: {
      fullName: {
        needs: { firstName: true, lastName: true },
        compute(user) {
          return `${user.firstName} ${user.lastName}`;
        },
      },
    },
  },

  // model — дополнительные методы к моделям
  model: {
    user: {
      async findByEmail(email: string) {
        return prisma.user.findUnique({ where: { email } });
      },
    },
  },

  // client — методы на уровне самого клиента
  client: {
    async healthCheck() {
      await prisma.$queryRaw`SELECT 1`;
      return true;
    },
  },
});

// Использование вычисляемого поля — fullName типизирован
const user = await prisma.user.findUnique({ where: { id: 1 } });
console.log(user?.fullName); // string | undefined — всё типобезопасно

// Вызов кастомного метода модели
const found = await prisma.user.findByEmail('alice@example.com');

Композиция расширений

// Расширения можно комбинировать цепочкой
const extendedPrisma = new PrismaClient()
  .$extends(loggingExtension)
  .$extends(softDeleteExtension)
  .$extends(tenantExtension);

// Или выносить в отдельные модули и реиспользовать
const loggingExtension = Prisma.defineExtension({
  query: {
    $allModels: {
      async $allOperations({ operation, model, args, query }) {
        const start = performance.now();
        const result = await query(args);
        console.log(`${model}.${operation}: ${performance.now() - start}ms`);
        return result;
      },
    },
  },
});

Подводные камни

  • $extends создаёт новый экземпляр клиента — переменная prisma после .$extends() это не тот же объект, нужно переприсваивать результат.
  • Внутри query расширения нельзя использовать исходный prisma для рекурсивных вызовов — нужно либо использовать query(args), либо аккуратно управлять ссылками.
  • Расширения типа result добавляют поля только в TypeScript-типы, не в БД — данные вычисляются в рантайме на Node.js стороне.
  • Middleware ($use) и $extends нельзя смешивать в одном клиенте — при миграции на расширения нужно полностью переписать middleware.
  • Расширения model не видны в транзакциях ($transaction) — кастомные методы нужно вызывать до входа в транзакцию или передавать клиент отдельно.
  • Prisma.defineExtension нужен для создания реиспользуемых расширений — иначе TypeScript не может правильно вывести тип расширенного клиента в другом файле.
  • Расширения не заменяют database-level триггеры: если нужна атомарность soft delete, лучше использовать базу данных или явные транзакции.

Common mistakes

  • Путает Prisma Client API с гарантиями базы данных: индексы, блокировки и isolation level не создаются магически.
  • Не объясняет, где в lifecycle находится устаревший middleware и Client extensions.
  • Не разделяет validation, authorization, business logic и persistence.
  • Игнорирует ошибки, лимиты входных данных, observability и тестирование.

What the interviewer is testing

  • Может объяснить устаревший middleware и Client extensions на примере кода.
  • Называет ключевые API: $extends(), $use.
  • Отделяет ORM/query builder поведение от реального поведения СУБД.
  • Видит production-риски: безопасность, отказоустойчивость, логирование и тесты.

Sources

Related topics