Перейти к содержимому

NestJS и Prisma

Как устроен бэкенд на NestJS и Prisma глазами .NET-разработчика: модули и DI, путь запроса через guards, interceptors и pipes, работа с базой через Prisma 7 и подготовка сервиса к продакшену. NestJS во многом повторяет ASP.NET Core, поэтому на собеседовании ждут не только «как», но и «чем отличается».

Перед этой темой: Рантайм Node.js, Внедрение зависимостей.

Junior+
Ответ

Что это

NestJS — фреймворк для бэкенда на Node.js и TypeScript. Он задаёт структуру приложения и даёт DI-контейнер, а сам HTTP обрабатывает Express или Fastify.

Из чего состоит

  • Модуль (@Module()) — группа связанных классов: контроллеры, провайдеры, импорты других модулей.
  • Контроллер (@Controller('orders')) — принимает запросы; @Get(':id') над методом задаёт маршрут.
  • Провайдер (@Injectable()) — сервис, репозиторий, клиент API. Контейнер создаёт его и передаёт в конструкторы.

Как пользоваться

  1. Пометьте сервис @Injectable() и добавьте его в providers модуля.
  2. Попросите сервис параметром конструктора контроллера.
  3. В main.ts вызовите NestFactory.create(AppModule) и app.listen(3000).

Частая ошибка

Забыть провайдер в providers или не экспортировать его из чужого модуля. Приложение не стартует: Nest can't resolve dependencies of the OrdersController (?).

Как устроено

  1. NestFactory.create(AppModule) обходит дерево модулей по imports.
  2. Для каждого класса контейнер читает design:paramtypes — типы параметров конструктора, которые TypeScript записывает при emitDecoratorMetadata.
  3. Провайдеры создаются в порядке зависимостей и принадлежат своему модулю.
  4. Модуль видит свои провайдеры и то, что экспортируют импортированные модули.

Соответствие ASP.NET Core

NestJS ASP.NET Core
main.ts, NestFactory.create Program.cs, WebApplication.CreateBuilder
@Module() с exports метод AddOrders(); инкапсуляции нет
@Controller() [ApiController]
providers: [OrdersService] builder.Services.AddScoped<OrdersService>()
DEFAULT, REQUEST, TRANSIENT Singleton, Scoped, Transient
Express или Fastify Kestrel

Ловушки

  • Интерфейсы. В рантайме их нет. Для интерфейса заводят токен (Symbol или строку): { provide: CLOCK, useClass: SystemClock } и @Inject(CLOCK). Другой вариант — абстрактный класс в роли токена.
  • import type. Тип, импортированный только как тип, стирается, и контейнер не узнаёт зависимость.
  • Циклы. Взаимозависимые модули требуют forwardRef(); обычно это знак, что общую часть пора вынести.

Правило

Модуль — на бизнес-область, наружу — только exports, интерфейсы — через токены.

Под капотом

  • Декоратор на классе заставляет TypeScript сохранить типы параметров конструктора; NestJS читает их через reflect-metadata.
  • Граф строится, а singleton-провайдеры создаются при старте, до listen. Нет зависимости — процесс не стартует. В .NET без ValidateOnBuild такая ошибка всплывёт только на запросе.
  • HTTP обрабатывает адаптер: @nestjs/platform-express (Express 5, по умолчанию) или @nestjs/platform-fastify (Fastify 5). Код на декораторах переносим, код с @Res() и Express-middleware — нет.

Цена

  • REQUEST «всплывает»: контроллер, зависящий от request-провайдера, тоже создаётся на каждый запрос вместе со всей цепочкой. Документация оценивает рост задержки примерно в 5% при аккуратном дизайне.
  • Request-провайдеры не получают хуков жизненного цикла.

Компромиссы

  • Singleton по умолчанию естественен для Node.js: поток один, гонок в памяти нет. Но поле с данными запроса в singleton перетрут параллельные запросы между await.
  • Пользователя и корреляционный id передают через AsyncLocalStorage (например, библиотека nestjs-cls).
  • Express даёт огромную экосистему middleware, Fastify по бенчмаркам заметно быстрее, но со своими плагинами и объектами запроса.

Как проверить в проде

  • Лог старта: Please make sure that the argument X at index [0] is available in the Y module указывает модуль и позицию параметра.
  • Цену REQUEST меряют нагрузочным тестом до и после.

Итог: NestJS — DI и модули поверх Express или Fastify; граф проверяется при старте, всё singleton по умолчанию, а состояние запроса живёт в AsyncLocalStorage.

Пример
import { Controller, Get, Inject, Injectable, Module, Param } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
// Интерфейса в рантайме нет, поэтому для него нужен токен
export interface Clock { now(): Date; }
export const CLOCK = Symbol('CLOCK');
@Injectable() // по умолчанию singleton на всё приложение
export class OrdersService {
constructor(@Inject(CLOCK) private readonly clock: Clock) {}
find(id: number) {
return { id, checkedAt: this.clock.now().toISOString() };
}
}
@Controller('orders')
export class OrdersController {
// Тип OrdersService попадает в метаданные design:paramtypes
constructor(private readonly orders: OrdersService) {}
@Get(':id')
find(@Param('id') id: string) {
return this.orders.find(Number(id)); // GET /orders/42 → { "id": 42, ... }
}
}
@Module({
controllers: [OrdersController],
providers: [OrdersService, { provide: CLOCK, useValue: { now: () => new Date() } }],
exports: [OrdersService], // без exports другие модули OrdersService не увидят
})
export class OrdersModule {}
async function bootstrap() {
const app = await NestFactory.create(OrdersModule); // Express по умолчанию
await app.listen(3000);
}
void bootstrap();
Middle+

Как обрабатывается запрос в NestJS: middleware, guards, interceptors, pipes, exception filters?

Заголовок раздела «Как обрабатывается запрос в NestJS: middleware, guards, interceptors, pipes, exception filters?»
Ответ

Что это

Между приходом запроса и методом контроллера NestJS вызывает несколько видов обработчиков:

Элемент Задача
Middleware общая обработка: CORS, заголовки безопасности
Guard пускать ли запрос: аутентификация, роли
Interceptor код до и после обработчика: замер времени, кеш
Pipe проверка и преобразование аргументов
Exception filter превращение исключения в HTTP-ответ

Как пользоваться

  1. Опишите тело запроса DTO-классом с декораторами class-validator: @IsEmail(), @IsInt(), @Min(1).
  2. Включите ValidationPipe глобально.
  3. Невалидное тело получит 400 со списком ошибок, метод контроллера не вызовется.

Частая ошибка

Описать DTO интерфейсом или type. В рантайме их нет, и ValidationPipe пропустит любое тело без проверки.

Как устроено

  1. Middleware: глобальные, затем модульные. Обработчик ещё не выбран.
  2. Guards: глобальные → контроллера → маршрута. false превращается в 403.
  3. Interceptors до обработчика — в том же порядке.
  4. Pipes: глобальные → контроллера → маршрута → параметра.
  5. Метод контроллера.
  6. Interceptors после — в обратном порядке, от маршрута к глобальным.
  7. При исключении ищется filter: маршрута, затем контроллера, затем глобальный. Срабатывает только один.

ValidationPipe

  • whitelist — удаляет поля без декораторов.
  • forbidNonWhitelisted — вместо удаления отвечает 400.
  • transform — превращает JSON в экземпляр DTO и приводит :id к number.

Сравнение с ASP.NET Core

NestJS ASP.NET Core
Guard authorization filter, [Authorize]
Interceptor action и result filters
ValidationPipe model binding и автоматический 400 от [ApiController]
Exception filter exception filter, IExceptionHandler

Ловушки

  • app.useGlobalGuards() создаёт объект вне DI. Регистрируйте { provide: APP_GUARD, useClass: AuthGuard }.
  • Без whitelist поле isAdmin из тела дойдёт до репозитория — mass assignment.

Под капотом

  • Guards, interceptors и filters получают ExecutionContext или ArgumentsHost — обёртку над HTTP, WebSocket и микросервисами. Один guard работает для любого транспорта.
  • ValidationPipe берёт тип параметра из метаданных метода, создаёт экземпляр через class-transformer и проверяет его class-validator.
  • next.handle() в interceptor возвращает RxJS Observable. Interceptor может вовсе не вызывать обработчик — так CacheInterceptor отдаёт ответ из кеша.

Цена

  • class-transformer и class-validator — рефлексия и аллокации на каждый запрос; на больших массивах в теле это заметно.
  • enableImplicitConversion приводит типы молча: строка "false" в query становится true.

Компромиссы

  • class-validator: один DTO-класс для валидации и документации, но декораторы дублируют типы. Standard Schema (StandardSchemaValidationPipe, @Body({ schema })) выводит тип из схемы.
  • @nestjs/swagger строит OpenAPI из контроллеров и DTO, а плагин CLI сам добавляет @ApiProperty().
  • Один глобальный filter задаёт единый формат ошибок, например ProblemDetails.

Как проверить в проде

  • Доля 400 и 403 по маршрутам показывает, где запросы отсекаются.
  • Время обработчика отделяют от всей цепочки interceptor-ом с замером или трассировкой.

Итог: каждый слой видит больше предыдущего; глобальные элементы — провайдерами APP_*, валидация — с whitelist, а схемы без классов в NestJS 12 — через Standard Schema.

Пример
import {
Body, CanActivate, Controller, ExecutionContext, Injectable, Module, Post, UseGuards, ValidationPipe,
} from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { IsEmail, IsInt, Min } from 'class-validator';
// DTO — класс: декораторы class-validator доживают до рантайма
export class CreateOrderDto {
@IsEmail() email!: string;
@IsInt() @Min(1) quantity!: number;
}
@Injectable()
export class ApiKeyGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<{ headers: Record<string, string | undefined> }>();
return req.headers['x-api-key'] === process.env.API_KEY; // false → 403 Forbidden
}
}
@Controller('orders')
@UseGuards(ApiKeyGuard)
export class OrdersController {
@Post()
create(@Body() dto: CreateOrderDto) {
return { ...dto, status: 'created' }; // сюда доходит только валидный DTO
}
}
@Module({ controllers: [OrdersController] })
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }));
await app.listen(3000);
}
void bootstrap();
// POST /orders { "email": "x", "quantity": 0, "isAdmin": true } → 400:
// property isAdmin should not exist, email must be an email, quantity must not be less than 1
Interceptor и exception filter
import {
ArgumentsHost, CallHandler, Catch, ExceptionFilter, ExecutionContext, Injectable, Logger, Module, NestInterceptor,
} from '@nestjs/common';
import { APP_FILTER, APP_INTERCEPTOR, HttpAdapterHost } from '@nestjs/core';
import { Observable, tap } from 'rxjs';
@Injectable()
export class TimingInterceptor implements NestInterceptor {
private readonly logger = new Logger('Timing');
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const started = Date.now(); // до обработчика
return next.handle().pipe(
// после обработчика: OrdersController.find 12ms
tap(() => this.logger.log(`${context.getClass().name}.${context.getHandler().name} ${Date.now() - started}ms`)),
);
}
}
export class OrderNotFoundError extends Error {}
// Доменная ошибка → 404: сервис ничего не знает про HTTP
@Catch(OrderNotFoundError)
export class OrderNotFoundFilter implements ExceptionFilter {
constructor(private readonly adapterHost: HttpAdapterHost) {}
catch(error: OrderNotFoundError, host: ArgumentsHost) {
const { httpAdapter } = this.adapterHost; // работает и с Express, и с Fastify
const body = { statusCode: 404, message: error.message };
httpAdapter.reply(host.switchToHttp().getResponse(), body, 404);
}
}
@Module({
providers: [
// Глобально и с DI: так они работают и в e2e-тестах
{ provide: APP_INTERCEPTOR, useClass: TimingInterceptor },
{ provide: APP_FILTER, useClass: OrderNotFoundFilter },
],
})
export class CoreModule {}
Middle+
Ответ

Что это

Prisma — ORM для Node.js и TypeScript. Модели описывают не классами, а в schema.prisma на собственном языке схем.

Как пользоваться

  1. Опишите модели User и Order и связь между ними в schema.prisma.
  2. npx prisma migrate dev --name init — создать SQL-миграцию и применить её к локальной базе.
  3. npx prisma generate — сгенерировать клиент с типами под ваши модели.
  4. В коде: prisma.user.findMany(), prisma.order.create({ data }).

Связанные данные

  • include: { orders: true } — загрузить связь целиком, как Include в EF Core.
  • select: { email: true } — взять только нужные поля, как проекция через Select.

Частая ошибка

Поправить schema.prisma и забыть prisma generate. Клиент не знает о новом поле, и TypeScript ругается, хотя столбец в базе уже есть.

Как устроено

  • migrate dev — только для разработки. Прогоняет историю миграций в теневой базе (shadow database), находит расхождения, пишет новую миграцию из изменений схемы и применяет её. При расхождении предлагает сбросить базу.
  • migrate deploy — для CI и прода: применяет неприменённые миграции, без теневой базы и сброса. Применённые записаны в таблице _prisma_migrations.
  • Seed с Prisma 7 запускают явно: prisma db seed.

Prisma и EF Core

Задача Prisma EF Core
Модель schema.prisma классы и DbContext
Новая миграция migrate dev dotnet ef migrations add
Связи include, select Include, Select
Изменение update() сразу пишет в базу трекер до SaveChanges
Сырой SQL $queryRaw по шаблону FromSql с интерполяцией

N+1

include даёт отдельный запрос на связь с WHERE ... IN, всего два запроса. Счётчики — _count, выборка по списку — where: { id: { in: ids } }. С preview-флагом relationJoins и relationLoadStrategy: 'join' связь грузится одним запросом с JOIN.

Транзакции

  • Вложенная запись (create с orders: { create: [...] }) — родитель и дети атомарно.
  • $transaction([a, b]) — независимые запросы, всё или ничего.
  • $transaction(async (tx) => ...) — логика между запросами; таймаут по умолчанию 5 с.

Ловушки

  • BigInt из базы не сериализуется в JSON: JSON.stringify бросает TypeError.
  • Нарушение уникальности приходит как PrismaClientKnownRequestError с кодом P2002; его переводят в 409 в exception filter.

Под капотом (Prisma 7)

  • Генератор prisma-client вместо prisma-client-js: клиент генерируется в ваш код (output обязателен), а не в node_modules.
  • Rust-движка нет: запрос в SQL переводит TypeScript, выполняет драйвер — pg через @prisma/adapter-pg. Пул и таймауты задаются настройками драйвера, и их умолчания отличаются от Prisma 6.
  • prisma.config.ts хранит путь к схеме, миграциям и URL базы; .env CLI сам больше не читает.
  • $use удалён, сквозную логику пишут через Client Extensions ($extends).
  • Пакет — ESM; для CommonJS-проекта NestJS в генераторе ставят moduleFormat = "cjs".

Цена

  • Нет change tracker: агрегат сохраняют явными вызовами в транзакции.
  • Интерактивная транзакция держит соединение всё время колбэка; HTTP-вызов внутри неё исчерпывает пул.
  • $queryRaw с шаблонной строкой параметризует значения, $queryRawUnsafe со склейкой строк открывает SQL-инъекции.

Компромиссы

ORM Сильная сторона Цена
Prisma точные типы, миграции из коробки кодогенерация, свой DSL
TypeORM сущности-классы с декораторами, ближе к EF Core тип результата не учитывает select
Drizzle схема на TypeScript, SQL-подобный API без генерации меньше абстракции над SQL

На октябрь 2026 тег latest пакета prisma указывает на RC 8-й версии, поэтому версию 7 фиксируют явно.

Как проверить в проде

  • log: ['query'] в PrismaClient показывает каждый SQL: N+1 видно по повторам.
  • pg_stat_statements показывает самые частые и дорогие запросы.

Итог: Prisma 7 — типизированный построитель запросов поверх драйвера; платите явными транзакциями и сырым SQL там, где DSL не хватает.

schema.prisma
generator client {
provider = "prisma-client"
output = "../src/generated/prisma"
moduleFormat = "cjs" // только для CommonJS-проекта
}
datasource db {
provider = "postgresql" // URL — в prisma.config.ts
}
model User {
id Int @id @default(autoincrement())
email String @unique
orders Order[]
}
model Order {
id Int @id @default(autoincrement())
total Decimal @db.Decimal(12, 2)
createdAt DateTime @default(now())
userId Int
user User @relation(fields: [userId], references: [id])
@@index([userId])
}
PrismaService и запросы
import { Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from './generated/prisma/client';
@Injectable() // singleton: один клиент и один пул соединений на процесс
export class PrismaService extends PrismaClient implements OnModuleInit, OnApplicationShutdown {
constructor(config: ConfigService) {
super({ adapter: new PrismaPg({ connectionString: config.getOrThrow<string>('DATABASE_URL') }) });
}
async onModuleInit() { await this.$connect(); } // база недоступна — старт падает
async onApplicationShutdown() { await this.$disconnect(); } // после закрытия HTTP-сервера
}
@Injectable()
export class OrdersRepository {
constructor(private readonly prisma: PrismaService) {}
// N+1: один запрос за пользователями и ещё по одному на каждого
async countsSlow() {
const users = await this.prisma.user.findMany();
return Promise.all(users.map(async (u) =>
({ email: u.email, orders: await this.prisma.order.count({ where: { userId: u.id } }) })));
}
// Без N+1: связь и счётчик в одном вызове клиента
async withOrders() {
return this.prisma.user.findMany({
select: { email: true, _count: { select: { orders: true } }, orders: { select: { id: true, total: true } } },
}); // тип результата: { email; _count: { orders }; orders: { id; total }[] }[]
}
// Интерактивная транзакция: оба запроса между BEGIN и COMMIT
async placeOrder(email: string, total: number) {
return this.prisma.$transaction(async (tx) => {
const user = await tx.user.upsert({ where: { email }, create: { email }, update: {} });
return tx.order.create({ data: { userId: user.id, total } });
}, { timeout: 5_000 }); // по умолчанию тоже 5 с; дольше — откат
}
}
Senior+
Ответ

Что это

Продакшен-сервис стартует с проверенной конфигурацией, пишет понятные логи, сообщает о своём состоянии и корректно останавливается.

Как пользоваться

  • Конфигурация. ConfigModule.forRoot({ isGlobal: true }) читает .env и переменные окружения; значения берут через config.getOrThrow('DATABASE_URL').
  • Логи. nestjs-pino пишет JSON с id запроса вместо текста в консоль.
  • Health check. @nestjs/terminus: эндпоинт проверяет базу и отвечает 200 или 503.
  • Остановка. app.enableShutdownHooks() в main.ts; без него хуки остановки при SIGTERM не вызываются.
  • Тесты. @nestjs/testing собирает модуль с подменёнными провайдерами.

Частая ошибка

Читать process.env.X по всему коду. Опечатка в имени переменной всплывёт в проде как undefined посреди запроса.

Как устроено

  1. Конфигурация. validationSchema проверяет переменные при старте. В NestJS 12 подходит любая Standard Schema, например Zod.
  2. Логи. LoggerModule.forRoot() логирует каждый запрос; app.useLogger(app.get(Logger)) направляет туда и логи самого Nest, а bufferLogs: true придерживает логи старта до готовности pino.
  3. Health checks. Readiness проверяет базу через PrismaHealthIndicator.pingCheck, liveness — только то, что процесс отвечает. Подробнее о разнице — в теме Наблюдаемость.
  4. Остановка. По SIGTERM: onModuleDestroy → beforeApplicationShutdown → закрытие HTTP-сервера → onApplicationShutdown.

Ловушки

  • app.useGlobalPipes() из main.ts не попадает в e2e-тест: тест собирает приложение из модуля. Глобальные pipes, guards и filters регистрируют через APP_PIPE, APP_GUARD, APP_FILTER.
  • Shell-форма CMD npm start в Dockerfile: сигнал получает shell, а не Node. Нужна exec-форма CMD ["node", "dist/main.js"].
  • На Windows сигналы поддерживаются ограниченно, остановку проверяют в Linux-контейнере.

Правило

Конфиг — со схемой, логи — pino в stdout, readiness — с базой, liveness — без неё, остановка — через shutdown hooks.

Под капотом

  • enableShutdownHooks() подписывается на сигналы процесса. Request-scoped провайдеры хуков не получают.
  • onModuleDestroy срабатывает до закрытия HTTP-сервера. Ресурсы, нужные запросам в полёте, — пул Prisma — закрывают в onApplicationShutdown.
  • gracefulShutdownTimeoutMs в TerminusModule.forRoot работает в beforeApplicationShutdown: health check уже отвечает 503, балансировщик убирает экземпляр, начатые запросы доделываются. Пауза плюс остановка должны быть меньше terminationGracePeriodSeconds (30 с по умолчанию).

Цена и производительность

  • pino пишет JSON быстрее большинства логгеров; pino-pretty — только для разработки.
  • Fastify ускоряет роутинг и сериализацию, но Express-middleware и @Res() с API Express придётся переписать.
  • CacheInterceptor из @nestjs/cache-manager кеширует GET-ответы; при нескольких экземплярах нужен общий Redis.

Компромиссы

  • Модульные тесты — overrideProvider с моками. E2E — createNestApplication() и supertest против PostgreSQL в Testcontainers после prisma migrate deploy: мок Prisma не поймает ошибку в SQL.
  • В NestJS 12 ESM-проекты по умолчанию используют Vitest, CommonJS — Jest.
  • Несколько сервисов — монорепо Nest CLI (apps/, libs/) или Nx.

Как проверить в проде

  • kill -TERM на staging: в логах 503 от readiness, завершение запросов, отключение базы, код выхода 0.
  • Задержки и 5xx — по логам pino и трассировке через OpenTelemetry или новый @nestjs/observe.

Итог: продакшен — это порядок остановки, проверяемый конфиг и тесты на настоящей базе, а не только код обработчиков.

main.ts и AppModule
import { Controller, Get, Module, ValidationPipe } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { APP_PIPE, NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import { HealthCheck, HealthCheckService, PrismaHealthIndicator, TerminusModule } from '@nestjs/terminus';
import { Logger, LoggerModule } from 'nestjs-pino';
import { z } from 'zod';
import { PrismaService } from './prisma.service';
@Controller('health')
export class HealthController {
constructor(private health: HealthCheckService, private db: PrismaHealthIndicator, private prisma: PrismaService) {}
@Get('live') // жив ли процесс — без внешних зависимостей
live() { return { status: 'ok' }; }
@Get('ready') @HealthCheck() // база недоступна или идёт остановка → 503
ready() { return this.health.check([() => this.db.pingCheck('database', this.prisma)]); }
}
@Module({
imports: [
ConfigModule.forRoot({ // нет DATABASE_URL — процесс падает при старте
isGlobal: true,
validationSchema: z.object({ DATABASE_URL: z.url(), PORT: z.coerce.number().default(3000) }),
}),
LoggerModule.forRoot({ pinoHttp: { level: process.env.LOG_LEVEL ?? 'info' } }), // JSON в stdout
TerminusModule.forRoot({ gracefulShutdownTimeoutMs: 10_000 }), // 10 с отдаём 503 до остановки
],
controllers: [HealthController],
providers: [PrismaService, { provide: APP_PIPE, useValue: new ValidationPipe({ whitelist: true, transform: true }) }],
})
export class AppModule {}
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter(), { bufferLogs: true });
app.useLogger(app.get(Logger)); // логи Nest тоже идут через pino
app.enableShutdownHooks(); // без этого SIGTERM не вызовет хуки остановки
await app.listen(process.env.PORT ?? 3000, '0.0.0.0'); // в контейнере слушаем все интерфейсы
}
void bootstrap();
E2E-тест
import { INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { afterAll, beforeAll, describe, it } from 'vitest';
import { AppModule } from './app.module';
import { PrismaService } from './prisma.service';
describe('health (e2e)', () => {
let app: INestApplication;
const down = () => Promise.reject(new Error('connection refused'));
beforeAll(async () => {
const moduleRef = await Test.createTestingModule({ imports: [AppModule] })
.overrideProvider(PrismaService) // как ConfigureTestServices в WebApplicationFactory
.useValue({ $runCommandRaw: down, $queryRawUnsafe: down }) // база «лежит»
.compile();
app = moduleRef.createNestApplication(); // main.ts не выполняется: APP_PIPE есть, useGlobalPipes нет
await app.init();
});
afterAll(() => app.close());
it('liveness does not depend on the database', () =>
request(app.getHttpServer()).get('/health/live').expect(200));
it('readiness without the database → 503', () =>
request(app.getHttpServer()).get('/health/ready').expect(503));
});

PrismaHealthIndicator пингует базу через $runCommandRaw, а для SQL-баз — через $queryRawUnsafe('SELECT 1'), поэтому мок подменяет оба метода.