NestJS — фреймворк для Node.js, который поверх Express задаёт структуру: код делится на модули, HTTP принимают контроллеры, логика живёт в провайдерах-сервисах, а зависимости между ними связывает встроенный контейнер внедрения зависимостей. Входные данные описываются классами-DTO с декораторами class-validator и проверяются ValidationPipe до вызова обработчика. Для проверки всего модуля Nest поднимает приложение в тесте через @nestjs/testing, а запросы шлёт supertest.
После материала вы сможете
- создать модуль с контроллером и сервисом через Nest CLI и объяснить, кто и когда создаёт экземпляр сервиса
- описать входные данные классом-DTO и включить
ValidationPipeсwhitelistиforbidNonWhitelisted - выбросить
NotFoundExceptionиConflictExceptionиз сервиса и получить правильные коды 404 и 409 - написать e2e-тест модуля на
@nestjs/testingиsupertest, который проверяет 201, 400, 404 и 409
Что знать заранее
Проверено на
NestJS 12.1 (@nestjs/core 12.1.2, @nestjs/cli 12.0.8)TypeScript 6.0.3class-validator 0.15.1, class-transformer 0.5.1Vitest 4.1, supertest 7.3Express 5.2 (@nestjs/platform-express)Node.js 24 LTS (24.18)
Сверено с документацией: 11 октября 2026 г.
Содержание
Зачем NestJS, если есть Express
Express даёт маршруты и middleware (промежуточные обработчики запроса) и больше ничего не навязывает. Пока API — пять обработчиков в одном файле, это плюс. Когда ресурсов становится двадцать, а разработчиков трое, каждый раскладывает код по-своему: где-то проверка данных внутри маршрута, где-то в отдельной функции, а подключение к базе импортируется напрямую из любого места. Тестировать такой код трудно — зависимости спрятаны в импортах.
NestJS решает организационную задачу. Под капотом по умолчанию тот же Express (пакет @nestjs/platform-express, можно переключиться на Fastify), но поверх него фреймворк задаёт единые правила: модуль объявляет, из чего состоит часть приложения; контроллер отвечает только за HTTP; сервис — за логику; объекты не создают друг друга сами, а получают зависимости через конструктор от контейнера Nest.
Маршрут
- Express
app.post("/leads", handler)- NestJS
- Метод класса с декоратором
@Post()в@Controller("leads")
Проверка тела запроса
- Express
- Своя middleware или вызов схемы в обработчике
- NestJS
- Класс-DTO +
ValidationPipe, обработчик получает уже проверенные данные
Связь обработчика и логики
- Express
- Прямой импорт модуля
- NestJS
- Внедрение зависимости через конструктор
Ошибки HTTP
- Express
- Свой обработчик ошибок
(err, req, res, next) - NestJS
- Встроенные исключения
NotFoundException,ConflictExceptionи слой обработки исключений
Тест модуля целиком
- Express
supertestна экспортированномapp- NestJS
Test.createTestingModule+supertest, с возможностью подменить провайдер
Цена структуры — больше файлов и понятий: декораторы, модули, провайдеры. Для небольшого сервиса из пары маршрутов Express честнее. NestJS окупается там, где у backend несколько доменов, команда и долгий срок жизни.
Проект: Nest CLI и структура
Проект создаёт Nest CLI. По документации NestJS 12 работает на Node.js 20.19+ (или 22.12+ в ветке 22), а генераторам CLI нужен Node.js 22.22.3+, 24.15+ или 26+. Флаги ниже делают создание неинтерактивным: менеджер пакетов npm, без git-репозитория и без модуля наблюдаемости @nestjs/observe.
npx @nestjs/cli@12 new leads-api --package-manager npm --skip-git --no-observe
cd leads-api
npx nest g module leads
npx nest g controller leads
npx nest g service leads
npm install class-validator class-transformer$ npx nest g module leads
CREATE src/leads/leads.module.ts (86 bytes)
UPDATE src/app.module.ts (331 bytes)
$ npx nest g controller leads
CREATE src/leads/leads.controller.ts (103 bytes)
CREATE src/leads/leads.controller.spec.ts (506 bytes)
UPDATE src/leads/leads.module.ts (177 bytes)
$ npx nest g service leads
CREATE src/leads/leads.service.ts (93 bytes)
CREATE src/leads/leads.service.spec.ts (474 bytes)
UPDATE src/leads/leads.module.ts (257 bytes)Что важно знать о шаблоне NestJS 12:
- Проект по умолчанию — ESM: в
package.jsonстоит"type": "module". Поэтому относительные импорты пишутся с расширением.js—import { LeadsService } from './leads.service.js', хотя файл называется.ts. Это требование Node.js для ES-модулей, его фиксирует руководство по миграции на v12. - Тест-раннер для ESM-проекта — Vitest (для CommonJS — Jest). Скрипты:
npm testзапускает unit-тесты*.spec.ts,npm run test:e2e— тесты*.e2e-spec.tsиз папкиtestпо отдельному конфигу. - Линтер — oxlint, форматирование — Prettier.
npm run buildвызываетnest buildи кладёт JavaScript вdist. - В
tsconfig.jsonвключеныexperimentalDecoratorsиemitDecoratorMetadata: на них держатся декораторы и внедрение зависимостей.strictPropertyInitializationвыключен, поэтому поля DTO без начального значения компилируются приstrict: true.
Сгенерированные app.controller.ts, app.service.ts и их тест с «Hello World!» в этом уроке удалены — корневой модуль только подключает модуль заявок. Шаблонный leads.controller.spec.ts тоже удалён: контроллер получает сервис через конструктор, и пустой тест без провайдера упал бы.
Модули, контроллеры, провайдеры и внедрение зависимостей
Начнём с модели данных. Статусы заявки — массив-константа, тип выводится из него: один источник правды и для TypeScript, и для проверки во время выполнения.
export const LEAD_STATUSES = ['new', 'in_progress', 'done'] as const;
export type LeadStatus = (typeof LEAD_STATUSES)[number];
export interface Lead {
id: string;
name: string;
phone: string;
message?: string;
status: LeadStatus;
createdAt: string;
}Сервис — это провайдер: класс с декоратором @Injectable(). Документация NestJS описывает провайдер как объект, который можно внедрить как зависимость. В сервисе живут правила предметной области: нельзя создать вторую открытую заявку с тем же телефоном, нельзя менять статус несуществующей заявки.
import { randomUUID } from 'node:crypto';
import {
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { CreateLeadDto } from './dto/create-lead.dto.js';
import type { Lead, LeadStatus } from './lead.js';
@Injectable()
export class LeadsService {
private readonly leads = new Map<string, Lead>();
create(dto: CreateLeadDto): Lead {
const open = [...this.leads.values()].find(
(lead) => lead.phone === dto.phone && lead.status !== 'done',
);
if (open) {
throw new ConflictException(
'An open lead with this phone already exists',
{
errorCode: 'LEAD_DUPLICATE',
},
);
}
const lead: Lead = {
id: randomUUID(),
name: dto.name,
phone: dto.phone,
message: dto.message,
status: 'new',
createdAt: new Date().toISOString(),
};
this.leads.set(lead.id, lead);
return lead;
}
findAll(): Lead[] {
return [...this.leads.values()];
}
findOne(id: string): Lead {
const lead = this.leads.get(id);
if (!lead) {
throw new NotFoundException(`Lead ${id} not found`);
}
return lead;
}
updateStatus(id: string, status: LeadStatus): Lead {
const lead = this.findOne(id);
lead.status = status;
return lead;
}
remove(id: string): void {
this.findOne(id);
this.leads.delete(id);
}
}Контроллер принимает HTTP и передаёт работу сервису. Префикс @Controller('leads') склеивается с путём метода: @Get(':id') обслуживает GET /leads/:id. Статус ответа по умолчанию — 200, для POST — 201; @HttpCode(204) меняет его для удаления, когда тела у ответа нет.
import {
Body,
Controller,
Delete,
Get,
HttpCode,
Param,
ParseUUIDPipe,
Patch,
Post,
} from '@nestjs/common';
import { CreateLeadDto } from './dto/create-lead.dto.js';
import { UpdateLeadStatusDto } from './dto/update-lead-status.dto.js';
import type { Lead } from './lead.js';
import { LeadsService } from './leads.service.js';
@Controller('leads')
export class LeadsController {
constructor(private readonly leadsService: LeadsService) {}
@Post()
create(@Body() dto: CreateLeadDto): Lead {
return this.leadsService.create(dto);
}
@Get()
findAll(): Lead[] {
return this.leadsService.findAll();
}
@Get(':id')
findOne(@Param('id', ParseUUIDPipe) id: string): Lead {
return this.leadsService.findOne(id);
}
@Patch(':id/status')
updateStatus(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: UpdateLeadStatusDto,
): Lead {
return this.leadsService.updateStatus(id, dto.status);
}
@Delete(':id')
@HttpCode(204)
remove(@Param('id', ParseUUIDPipe) id: string): void {
this.leadsService.remove(id);
}
}Модуль связывает части. В controllers — то, что Nest должен создать и зарегистрировать как маршруты, в providers — то, что контейнер может внедрять внутри модуля. Корневой AppModule импортирует модуль заявок.
import { Module } from '@nestjs/common';
import { LeadsController } from './leads.controller.js';
import { LeadsService } from './leads.service.js';
@Module({
controllers: [LeadsController],
providers: [LeadsService],
})
export class LeadsModule {}import { Module } from '@nestjs/common';
import { LeadsModule } from './leads/leads.module.js';
@Module({
imports: [LeadsModule],
})
export class AppModule {}- 01HTTP-запрос
- 02ValidationPipe
- 03LeadsController
- 04LeadsService
- 05Map в памяти
Как работает внедрение. Конструктор constructor(private readonly leadsService: LeadsService) не создаёт сервис, а объявляет, что он нужен. При запуске Nest читает метаданные типов параметров конструктора (их записывает TypeScript благодаря emitDecoratorMetadata), находит LeadsService среди провайдеров модуля, создаёт его и передаёт в контроллер. По документации область жизни по умолчанию — singleton: экземпляр создаётся один раз и общий для всех, кто от него зависит. Поэтому хранилище Map в сервисе живёт столько же, сколько процесс.
Модули инкапсулированы: провайдер виден только внутри своего модуля. Если сервис заявок понадобится, например, модулю уведомлений, его нужно добавить в exports модуля заявок, а модуль заявок — в imports модуля уведомлений. Без этого Nest при запуске сообщит, что не может разрешить зависимость.
DTO и ValidationPipe
DTO (Data Transfer Object) — описание формы данных, которые приходят в запросе. В NestJS DTO делают классами, а не интерфейсами: интерфейсы TypeScript стираются при компиляции, а pipe-объектам Nest нужен тип во время выполнения. Правила проверки задают декораторы из class-validator.
import {
IsNotEmpty,
IsOptional,
IsString,
Matches,
MaxLength,
} from 'class-validator';
export class CreateLeadDto {
@IsString()
@IsNotEmpty()
@MaxLength(100)
name: string;
@Matches(/^\+7\d{10}$/, {
message: 'phone must be in the format +7XXXXXXXXXX',
})
phone: string;
@IsOptional()
@IsString()
@MaxLength(1000)
message?: string;
}import { IsIn } from 'class-validator';
import { LEAD_STATUSES, type LeadStatus } from '../lead.js';
export class UpdateLeadStatusDto {
@IsIn(LEAD_STATUSES)
status: LeadStatus;
}Pipe (канал преобразования) в Nest выполняется непосредственно перед обработчиком: получает аргументы, проверяет или преобразует их и либо пропускает дальше, либо бросает исключение — тогда обработчик не вызывается. ValidationPipe превращает тело запроса в экземпляр DTO и запускает правила class-validator. ParseUUIDPipe из контроллера работает так же, но для одного параметра пути.
import { type INestApplication, ValidationPipe } from '@nestjs/common';
export function configureApp(app: INestApplication): void {
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
}),
);
}import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { configureApp } from './setup.js';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
configureApp(app);
await app.listen(process.env.PORT ?? 3000);
}
await bootstrap();whitelist: true— поля, которых нет в DTO, удаляются из объекта. Без этого клиент мог бы прислатьroleилиstatusи, если код где-то разворачивает тело целиком, записать их.forbidNonWhitelisted: true— вместо тихого удаления лишнее поле приводит к ответу 400. Клиент сразу узнаёт об опечатке в имени поля.- Формат ошибок по умолчанию —
errorFormat: 'list', массив строк вmessage. В NestJS 12 появилась опция'grouped'— объект, где ключ — путь к полю.
$ curl -i -X POST localhost:3000/leads -H 'Content-Type: application/json' \
-d '{"name":"Aida","phone":"+77011234567"}'
HTTP/1.1 201 Created
{"id":"546c1f10-d53a-4e09-af7a-3c8a8e17cd39","name":"Aida","phone":"+77011234567","status":"new","createdAt":"2026-10-11T23:12:57.267Z"}
$ curl -i -X POST localhost:3000/leads -H 'Content-Type: application/json' \
-d '{"name":"","phone":"8701","role":"admin"}'
HTTP/1.1 400 Bad Request
{"message":["property role should not exist","name should not be empty","phone must be in the format +7XXXXXXXXXX"],"error":"Bad Request","statusCode":400}Если в проекте уже есть схемы Zod — например, из урока про Express, — в NestJS 12 есть StandardSchemaValidationPipe: он проверяет параметры, у которых в декораторе указана схема (@Body({ schema: createLeadSchema })), и пропускает остальные. Оба pipe можно включить одновременно.
Исключения: 404, 409 и единый формат
В Nest есть встроенный слой обработки исключений: если обработчик или сервис бросает HttpException или его наследника, клиент получает ответ с соответствующим кодом и JSON-телом. Любое другое исключение превращается в 500 с телом {"statusCode": 500, "message": "Internal server error"} — детали ошибки наружу не утекают.
BadRequestException
- Код
- 400
- Когда бросать
- Данные формально неверны, а pipe этого не поймал
UnauthorizedException
- Код
- 401
- Когда бросать
- Нет или неверна аутентификация
ForbiddenException
- Код
- 403
- Когда бросать
- Пользователь известен, но прав нет
NotFoundException
- Код
- 404
- Когда бросать
- Ресурса с таким идентификатором нет
ConflictException
- Код
- 409
- Когда бросать
- Запрос противоречит текущему состоянию: дубликат, гонка изменений
UnprocessableEntityException
- Код
- 422
- Когда бросать
- Данные корректны по форме, но не проходят бизнес-проверку — если команда разделяет 400 и 422
Вторым аргументом исключению можно передать errorCode — стабильный машиночитаемый код, который появляется в теле ответа. Текст message для людей может меняться, а клиент (сайт или мобильное приложение) ветвится по errorCode.
$ curl -i -X POST localhost:3000/leads -H 'Content-Type: application/json' \
-d '{"name":"Aida","phone":"+77011234567"}'
HTTP/1.1 409 Conflict
{"message":"An open lead with this phone already exists","error":"Conflict","statusCode":409,"errorCode":"LEAD_DUPLICATE"}
$ curl -i localhost:3000/leads/42
HTTP/1.1 400 Bad Request
{"message":"Validation failed (uuid is expected)","error":"Bad Request","statusCode":400}
$ curl -i localhost:3000/leads/8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b
HTTP/1.1 404 Not Found
{"message":"Lead 8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b not found","error":"Not Found","statusCode":404}Формат по умолчанию — statusCode, message, error. Если API уже отдаёт ошибки по стандарту Problem Details (RFC 9457) с типом application/problem+json — как в уроке про Express, — формат меняют фильтром исключений: класс с декоратором @Catch(), реализующий ExceptionFilter, подключается глобально через app.useGlobalFilters(). Такой фильтр — последнее упражнение урока.
Тесты: unit-тест сервиса и e2e-тест модуля
Test.createTestingModule принимает те же метаданные, что @Module(), а compile() асинхронно собирает граф зависимостей. Для unit-теста сервиса достаточно одного провайдера.
import { ConflictException, NotFoundException } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import { LeadsService } from './leads.service.js';
describe('LeadsService', () => {
let service: LeadsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
providers: [LeadsService],
}).compile();
service = moduleRef.get(LeadsService);
});
it('creates a lead with status "new"', () => {
const lead = service.create({ name: 'Aida', phone: '+77011234567' });
expect(lead.status).toBe('new');
expect(service.findOne(lead.id)).toEqual(lead);
});
it('rejects a second open lead with the same phone', () => {
service.create({ name: 'Aida', phone: '+77011234567' });
expect(() =>
service.create({ name: 'Aida', phone: '+77011234567' }),
).toThrow(ConflictException);
});
it('allows a new lead once the previous one is done', () => {
const first = service.create({ name: 'Aida', phone: '+77011234567' });
service.updateStatus(first.id, 'done');
const second = service.create({ name: 'Aida', phone: '+77011234567' });
expect(second.id).not.toBe(first.id);
});
it('throws NotFoundException for an unknown id', () => {
expect(() =>
service.findOne('8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b'),
).toThrow(NotFoundException);
});
});E2e-тест (сквозной, end-to-end) поднимает весь AppModule: createNestApplication() создаёт настоящее приложение, init() инициализирует его, а supertest отправляет HTTP-запросы в app.getHttpServer() без открытия порта. После каждого теста app.close() освобождает ресурсы; новое приложение в beforeEach — это и чистое хранилище для каждого теста.
import { type INestApplication } from '@nestjs/common';
import { Test } from '@nestjs/testing';
import request from 'supertest';
import { type App } from 'supertest/types.js';
import { AppModule } from '../src/app.module.js';
import { configureApp } from '../src/setup.js';
describe('Leads (e2e)', () => {
let app: INestApplication<App>;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleRef.createNestApplication();
configureApp(app);
await app.init();
});
afterEach(async () => {
await app.close();
});
it('POST /leads creates a lead (201)', async () => {
const res = await request(app.getHttpServer())
.post('/leads')
.send({ name: 'Aida', phone: '+77011234567' })
.expect(201);
expect(res.body).toMatchObject({ name: 'Aida', status: 'new' });
expect(res.body.id).toEqual(expect.any(String));
});
it('POST /leads rejects an invalid body (400)', async () => {
const res = await request(app.getHttpServer())
.post('/leads')
.send({ name: '', phone: '8701', role: 'admin' })
.expect(400);
expect(res.body.message).toEqual(
expect.arrayContaining([
'property role should not exist',
'name should not be empty',
'phone must be in the format +7XXXXXXXXXX',
]),
);
});
it('POST /leads returns 409 for a duplicate open lead', async () => {
const lead = { name: 'Aida', phone: '+77011234567' };
await request(app.getHttpServer()).post('/leads').send(lead).expect(201);
const res = await request(app.getHttpServer())
.post('/leads')
.send(lead)
.expect(409);
expect(res.body.errorCode).toBe('LEAD_DUPLICATE');
});
it('GET /leads/:id returns 400 for a non-UUID and 404 for an unknown id', async () => {
await request(app.getHttpServer()).get('/leads/42').expect(400);
await request(app.getHttpServer())
.get('/leads/8f14e45f-ceea-4e7a-9b1f-1c2d3e4f5a6b')
.expect(404);
});
it('PATCH status and DELETE (204)', async () => {
const { body: lead } = await request(app.getHttpServer())
.post('/leads')
.send({ name: 'Aida', phone: '+77011234567' })
.expect(201);
await request(app.getHttpServer())
.patch(`/leads/${lead.id}/status`)
.send({ status: 'archived' })
.expect(400);
const { body: updated } = await request(app.getHttpServer())
.patch(`/leads/${lead.id}/status`)
.send({ status: 'in_progress' })
.expect(200);
expect(updated.status).toBe('in_progress');
await request(app.getHttpServer()).delete(`/leads/${lead.id}`).expect(204);
await request(app.getHttpServer()).get(`/leads/${lead.id}`).expect(404);
});
});$ npm test -- --reporter=verbose
RUN v4.1.11 …/leads-api
✓ src/leads/leads.service.spec.ts > LeadsService > creates a lead with status "new" 408ms
✓ src/leads/leads.service.spec.ts > LeadsService > rejects a second open lead with the same phone 6ms
✓ src/leads/leads.service.spec.ts > LeadsService > allows a new lead once the previous one is done 5ms
✓ src/leads/leads.service.spec.ts > LeadsService > throws NotFoundException for an unknown id 11ms
Test Files 1 passed (1)
Tests 4 passed (4)
$ npm run test:e2e -- --reporter=verbose
RUN v4.1.11 …/leads-api
✓ test/leads.e2e-spec.ts > Leads (e2e) > POST /leads creates a lead (201) 196ms
✓ test/leads.e2e-spec.ts > Leads (e2e) > POST /leads rejects an invalid body (400) 17ms
✓ test/leads.e2e-spec.ts > Leads (e2e) > POST /leads returns 409 for a duplicate open lead 17ms
✓ test/leads.e2e-spec.ts > Leads (e2e) > GET /leads/:id returns 400 for a non-UUID and 404 for an unknown id 16ms
✓ test/leads.e2e-spec.ts > Leads (e2e) > PATCH status and DELETE (204) 24ms
Test Files 1 passed (1)
Tests 5 passed (5)Зачем configureApp(app) в тесте. Тест не запускает main.ts, поэтому всё, что настроено там через app.useGlobalPipes, в тестовом приложении отсутствует. Мы проверили это, закомментировав вызов: приложение принимает невалидные данные.
$ npm run test:e2e
× POST /leads rejects an invalid body (400) 14ms
× PATCH status and DELETE (204) 15ms
FAIL test/leads.e2e-spec.ts > Leads (e2e) > POST /leads rejects an invalid body (400)
Error: expected 400 "Bad Request", got 201 "Created"
FAIL test/leads.e2e-spec.ts > Leads (e2e) > PATCH status and DELETE (204)
Error: expected 400 "Bad Request", got 200 "OK"
Tests 2 failed | 3 passed (5)Отсюда правило: глобальные настройки — в одной функции, которую вызывают и main.ts, и тесты. Альтернатива — регистрировать глобальный pipe как провайдер модуля через токен APP_PIPE, тогда он попадает в приложение вместе с модулем.
Частые ошибки и ограничения
При запуске: Nest can't resolve dependencies of the LeadsController (?)
- Причина
- Сервис не указан в
providersмодуля или находится в другом модуле безexports/imports - Исправление
- Добавить провайдер в свой модуль или экспортировать его и импортировать модуль
Невалидные данные проходят, в ответе 201
- Причина
- Нет
ValidationPipe(часто — только в тесте) или DTO импортирован черезimport type - Исправление
- Общая функция настройки для
main.tsи тестов; обычный импорт DTO
При сборке: TS2307: Cannot find module './leads.service'
- Причина
- Относительный импорт без
.jsв ESM-проекте (moduleResolution: nodenext) - Исправление
- Писать
./leads.service.js
@Param('id') id: number содержит строку
- Причина
- Параметры пути и строки запроса всегда приходят строками
- Исправление
ParseIntPipe/ParseUUIDPipeилиtransform: trueуValidationPipe
Данные пропадают после перезапуска
- Причина
- Хранилище —
Mapв памяти singleton-сервиса - Исправление
- Это учебное упрощение; дальше — база данных через репозиторий
- Каждая область — отдельный модуль; корневой модуль только импортирует их
- Контроллер не содержит правил: только маршрут, параметры, вызов сервиса
- Тело и параметры проверяются до обработчика: DTO +
ValidationPipe,Parse…Pipeдля пути - Ошибки — встроенные исключения с правильным кодом, для клиента — стабильный
errorCode - Глобальные pipe и фильтры подключаются одинаково в
main.tsи в e2e-тесте - E2e-тест проверяет не только успех, но и 400, 404, 409
Чего в уроке нет и что понадобится в настоящем сервисе: хранилище в базе данных с репозиторием, аутентификация и права (guards), конфигурация через переменные окружения, документация OpenAPI. Каждая из этих тем — отдельный урок направления; структура «модуль → контроллер → сервис» остаётся той же.
Упражнения
Следующий шаг — урок о слоях backend-приложения: в нём хранилище уходит из сервиса в репозиторий, а правила и транзакции остаются в сервисе. Тесты backend с настоящей базой данных — в уроке о тестировании backend.
Как проверено7 · пройденосверено 11 октября 2026 г.
- запущено, работает
Проект leads-api: nest new (@nestjs/cli 12.0.8, --package-manager npm --skip-git --no-observe), nest g module/controller/service leads
реальный вывод генераторов скопирован в статью; scratchpad/kb/verify/nestjs-vvedenie/leads-api
- запущено, работает
lead.ts, DTO, leads.service.ts, leads.controller.ts, leads.module.ts, app.module.ts, setup.ts, main.ts
npm run build (nest build) — без ошибок; tsc --noEmit (strict) — без ошибок; npm run lint (oxlint) — без замечаний; NestJS 12.1.2, TypeScript 6.0.3, class-validator 0.15.1, Node.js 24.18.0
- запущено, работает
Ответы curl (201, 400, 409, 400 uuid, 404)
node dist/main.js на временном порту, реальные ответы скопированы (localhost:3000 в статье вместо тестового порта)
- запущено, работает
leads.service.spec.ts (unit) и test/leads.e2e-spec.ts (e2e)
npm test — 4/4; npm run test:e2e — 5/5 (Vitest 4.1.11, supertest 7.3.1)
- запущено, работает
Ловушка без configureApp в e2e и упражнение «repeat» (без forbidNonWhitelisted)
без configureApp: 2 failed | 3 passed (201 вместо 400, 200 вместо 400); без forbidNonWhitelisted: 1 failed | 4 passed — пропала строка 'property role should not exist'
- запущено, работает
Таблица ошибок: UnknownDependencies и импорт без .js
провайдер убран из модуля → при запуске «Nest can't resolve dependencies of the LeadsController (?)»; импорт без .js → TS2307 при nest build
- запущено, работает
Решения упражнений modify (ListLeadsQueryDto + @Query) и apply (ProblemDetailsFilter)
копия проекта leads-api-ex: test/exercises.e2e-spec.ts 2/2 + прежние 5/5, tsc --noEmit и nest build без ошибок
«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.