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

Первая программа на NestJS

Разработчику Архитектору

Первая программа на NestJS

NestJS — фреймворк для серверного TypeScript на Node.js. Он добавляет:

  • модули — логические блоки приложения;
  • dependency injection (DI) — внедрение зависимостей через конструктор;
  • декораторы @Controller, @Get — описание HTTP-маршрутов.

Структура похожа на Angular и Spring. Под капотом по умолчанию работает Express (или Fastify при переключении адаптера).

Если вы уже прошли Первая программа на Node.js и Express — middleware, маршруты и ошибки, NestJS — следующий шаг для типизированного backend с предсказуемой архитектурой.

ШагТемаРезультат
1@nestjs/cli, nest newКаркас проекта
2AppModule, AppControllerТочка входа и маршрут
3NotesService + DIБизнес-логика отдельно от HTTP
4DTO и ValidationPipeВалидация тела запроса
5CRUD /notesУчебное REST API
6Guards и фильтры ошибокЕдиный формат ответов
7Сборка и деплойProduction-ready сервис
МатериалЗачем
Первая программа на Node.jsnpm, Express, REST "Заметки"
Express — middlewareRouter, CORS, errorHandler
npm — команды и lock-файлыinstall, scripts, audit
TypeScript — первая программатипы, интерфейсы, strict
Prisma ORM — первая программазамена in-memory store
Fullstack на JavaScriptCORS, прокси, два порта

Навигация по блоку Node.js

Редактор и терминал

Команды удобно выполнять во вкладке Терминал VS Code (Ctrl+`). TypeScript-подсветка и отладка — статья "Отладка".


Что такое NestJS и зачем он нужен

В Express маршруты, валидация и бизнес-логика часто оказываются в одном файле. На маленьком API это терпимо. Когда эндпоинтов десятки, нужны правила:

  • контроллер только принимает HTTP и вызывает сервис;
  • сервис не знает про req и res;
  • зависимости (БД, кэш) подставляются через конструктор, а не создаются через new в каждом месте.

NestJS задаёт эту структуру из коробки через модули, провайдеры и DI-контейнер.

СлойОтветственность
ControllerМаршрут, HTTP-код, DTO
ServiceБизнес-логика, правила
ModuleРегистрация классов в DI
PipeВалидация и преобразование
GuardАвторизация (позже)

Шаг 1 — подготовка окружения

Нужны Node.js LTS (20 или 22) и npm:

node -v
npm -v

Установка CLI глобально (или через npx без глобальной установки):

npm i -g @nestjs/cli
nest --version

Альтернатива без -g:

npx @nestjs/cli new notes-nest --strict

Создание проекта:

nest new notes-nest --strict
cd notes-nest
npm run start:dev

Разбор:

  • nest new генерирует TypeScript-проект с src/main.ts, app.module.ts, тестами Jest.
  • Флаг --strict включает строгие правила TypeScript — рекомендуется для новых проектов. Подробнее о strict — в разделе TypeScript.
  • start:dev — watch-режим: сохранили файл — сервер перезапустился.
  • По умолчанию сервер слушает http://localhost:3000.

Откройте http://localhost:3000 — ответ Hello World! из AppController.

TypeScript обязателен

NestJS пишут на TypeScript. Если синтаксис interface, ! и декораторов незнаком — сначала пройдите Первая программа на TypeScript.


Шаг 2 — структура проекта

После nest new в каталоге появляется:

notes-nest/
src/
main.ts ← bootstrap, глобальные pipes
app.module.ts ← корневой модуль
app.controller.ts ← HTTP-маршруты (шаблон)
app.service.ts ← логика (шаблон)
app.controller.spec.ts
test/
nest-cli.json
tsconfig.json
package.json
ФайлРоль
main.tsСоздаёт приложение, слушает порт
app.module.tsКорневой @Module — точка сборки
*.controller.tsHTTP-слой
*.service.tsБизнес-логика, @Injectable
nest-cli.jsonНастройки CLI (генерация, webpack)

Три ключевых понятия:

  • Модуль (@Module) собирает контроллеры и провайдеры.
  • Контроллер принимает HTTP-запросы.
  • Сервис (@Injectable) — переиспользуемая логика; Nest сам подставляет зависимости в конструктор (DI).

Точка входа main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

Разбор:

  • NestFactory.create(AppModule) поднимает HTTP-сервер поверх Express.
  • process.env.PORT ?? 3000 — порт из окружения для хостинга (как в 262).
  • bootstrap() — асинхронная функция; Nest ждёт готовности приложения.

Шаг 3 — модуль заметок с нуля

Создайте feature-модуль через CLI:

nest generate module notes
nest generate service notes
nest generate controller notes

Или вручную — три файла в src/notes/.

NotesService — данные в памяти

src/notes/notes.service.ts:

import { Injectable, NotFoundException } from '@nestjs/common';

export interface Note {
id: number;
text: string;
}

@Injectable()
export class NotesService {
private notes: Note[] = [
{ id: 1, text: 'Купить молоко' },
{ id: 2, text: 'Изучить NestJS' },
];
private nextId = 3;

findAll(): Note[] {
return [...this.notes];
}

findOne(id: number): Note {
const note = this.notes.find((n) => n.id === id);
if (!note) throw new NotFoundException(`Note ${id} not found`);
return note;
}

create(text: string): Note {
const note = { id: this.nextId++, text };
this.notes.push(note);
return note;
}

remove(id: number): void {
const index = this.notes.findIndex((n) => n.id === id);
if (index === -1) throw new NotFoundException(`Note ${id} not found`);
this.notes.splice(index, 1);
}
}

Разбор:

  • @Injectable() регистрирует класс в DI-контейнере Nest.
  • NotFoundException — встроенное исключение; Nest вернёт 404 с JSON-телом.
  • [...this.notes] — копия массива, чтобы снаружи нельзя было мутировать внутреннее хранилище.

NotesController — HTTP-слой

src/notes/notes.controller.ts:

import {
Body,
Controller,
Delete,
Get,
Param,
ParseIntPipe,
Post,
HttpCode,
} from '@nestjs/common';
import { NotesService } from './notes.service';

@Controller('notes')
export class NotesController {
constructor(private readonly notes: NotesService) {}

@Get()
findAll() {
return this.notes.findAll();
}

@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.notes.findOne(id);
}

@Post()
create(@Body('text') text: string) {
return this.notes.create(text);
}

@Delete(':id')
@HttpCode(204)
remove(@Param('id', ParseIntPipe) id: number) {
this.notes.remove(id);
}
}

Разбор:

  • @Controller('notes') — префикс URL: все маршруты начинаются с /notes.
  • constructor(private readonly notes: NotesService) — DI: Nest создаёт сервис и передаёт сюда.
  • ParseIntPipe превращает строку :id из URL в число; при ошибке — 400.
  • @HttpCode(204) — успешный DELETE без тела (как в 262).

NotesModule — регистрация

src/notes/notes.module.ts:

import { Module } from '@nestjs/common';
import { NotesController } from './notes.controller';
import { NotesService } from './notes.service';

@Module({
controllers: [NotesController],
providers: [NotesService],
})
export class NotesModule {}

Подключите модуль в app.module.ts:

import { Module } from '@nestjs/common';
import { NotesModule } from './notes/notes.module';

@Module({
imports: [NotesModule],
})
export class AppModule {}

Можно удалить шаблонные AppController и AppService из AppModule, если они больше не нужны.


Шаг 4 — проверка REST API

Запуск:

npm run start:dev

Проверка через curl (тот же сценарий, что в 262):

curl http://localhost:3000/notes
curl http://localhost:3000/notes/1
curl -X POST http://localhost:3000/notes \
-H "Content-Type: application/json" \
-d '{"text":"Новая заметка"}'
curl -X DELETE http://localhost:3000/notes/1
curl http://localhost:3000/notes
МетодПутьОжидание
GET/notes200, массив
GET/notes/1200, объект
POST/notes201, созданная заметка
DELETE/notes/1204, пустое тело
GET/notes/999404, JSON с сообщением
Данные в памяти

После перезапуска npm run start:dev список снова из шаблона. Для постоянного хранения подключите Prisma или Drizzle.


Шаг 5 — DTO и ValidationPipe

DTO (Data Transfer Object) — класс, описывающий форму тела запроса. Вместо ручных if (!text) Nest проверяет поля автоматически.

Установите пакеты:

npm i class-validator class-transformer

src/notes/dto/create-note.dto.ts:

import { IsString, MinLength, MaxLength } from 'class-validator';

export class CreateNoteDto {
@IsString()
@MinLength(1)
@MaxLength(500)
text!: string;
}

В main.ts включите глобальный pipe:

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
ОпцияСмысл
whitelist: trueУбирает лишние поля из body
forbidNonWhitelisted: trueОшибка при неизвестных полях
transform: trueПреобразует типы (строка → число для @Param)

В контроллере:

import { CreateNoteDto } from './dto/create-note.dto';

@Post()
create(@Body() dto: CreateNoteDto) {
return this.notes.create(dto.text);
}

Проверка невалидного запроса:

curl -X POST http://localhost:3000/notes \
-H "Content-Type: application/json" \
-d '{"text":""}'

Ответ 400 с массивом message — без ручных проверок в контроллере.


Шаг 6 — health-check и CORS

Эндпоинт /health

src/health/health.controller.ts:

import { Controller, Get } from '@nestjs/common';

@Controller('health')
export class HealthController {
@Get()
check() {
return { status: 'ok', ts: new Date().toISOString() };
}
}

Зарегистрируйте в отдельном HealthModule или добавьте контроллер в AppModule.

CORS для фронтенда

Если Svelte или React на порту 5173 обращается к API на 3000, включите CORS в main.ts:

const app = await NestFactory.create(AppModule);
app.enableCors({ origin: 'http://localhost:5173' });

Подробнее про cross-origin — Fullstack на JavaScript — API и фронтенд и Express — middleware.


Шаг 7 — единый формат ошибок

Nest по умолчанию возвращает JSON при исключениях. Для учебного API можно добавить фильтр:

src/common/http-exception.filter.ts:

import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common';

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse();
const status = exception.getStatus();
const body = exception.getResponse();

res.status(status).json({
statusCode: status,
error: typeof body === 'string' ? body : (body as { message?: unknown }).message,
timestamp: new Date().toISOString(),
});
}
}

Подключение в main.ts:

app.useGlobalFilters(new HttpExceptionFilter());

Полный walkthrough — от Express к NestJS

Если вы делали API "Заметки" на Express (262), вот соответствие слоёв:

ExpressNestJS
app.get('/notes', ...)@Get() в NotesController
express.json()Встроено; DTO + ValidationPipe
Массив notes в server.jsNotesService с приватным массивом
app.use(errorHandler)@Catch() фильтры
Router в отдельном файле@Module + @Controller

NestJS и Express — когда что выбирать

СценарийNestJSExpress вручную
Команда знает Angular или Springпривычные паттерныпроще старт, без DI
Крупный REST или GraphQL backendмодули, guards, pipesнужна дисциплина в структуре
Микроскрипт на 3 маршрутаизбыточенподходит
Строгая TypeScript-архитектураиз коробкивозможна, но вручную
Нужен Fastify вместо ExpressNestFactory.create(..., { adapter })отдельная настройка

NestJS не заменяет понимание HTTP и Express — он организует код поверх них. Middleware из 263 работает и в Nest через app.use().


Упражнения

  1. Добавьте PATCH /notes/:id для редактирования текста. Создайте UpdateNoteDto с @IsOptional() и @MinLength(1).
  2. Вынесите NotesService в отдельный NotesRepository (интерфейс + in-memory реализация) — подготовка к Prisma.
  3. Напишите e2e-тест test/notes.e2e-spec.ts через @nestjs/testing и supertest.
  4. Подключите Prisma ORM — первая программа вместо массива в NotesService.
  5. Добавьте глобальный префикс app.setGlobalPrefix('api') — все маршруты станут /api/notes.

Частые ошибки и troubleshooting

СимптомПричинаЧто сделать
Nest can't resolve dependencies of NotesControllerСервис не в providers модуляДобавьте NotesService в @Module({ providers: [...] })
404 на /notesМодуль не импортирован в AppModuleimports: [NotesModule]
Validation не срабатываетНет ValidationPipe в main.tsapp.useGlobalPipes(new ValidationPipe(...))
Cannot find module '@nestjs/common'Нет npm installnpm install в корне проекта
Порт занятДругой процесс на 3000PORT=3001 npm run start:dev или завершите старый процесс
TypeScript ругается на декораторыНет experimentalDecoratorsВ Nest-шаблоне уже включено в tsconfig.json
POST возвращает пустой text@Body('text') вместо DTOИспользуйте @Body() dto: CreateNoteDto
CORS в браузереФронт на другом портуapp.enableCors()264

FAQ

Нужен ли NestJS для первого backend-проекта?

Нет. Начните с 262 и Express. NestJS имеет смысл, когда проект растёт и нужна структура.

Можно ли писать на чистом JavaScript?

Технически да, но экосистема и документация ориентированы на TypeScript. Для NestJS лучше освоить TypeScript.

Express или Fastify под капотом?

По умолчанию Express. Fastify быстрее на бенчмарках; переключение — через @nestjs/platform-fastify.

Как NestJS связан с Angular?

Общие идеи: модули, DI, декораторы. Синтаксис похож, но NestJS — только сервер.

Где хранить секреты?

В переменных окружения (process.env), не в коде. Для локальной разработки — файл .env (добавьте в .gitignore).


Production и деплой

Сборка

npm run build
npm run start:prod

build компилирует TypeScript в dist/. start:prod запускает node dist/main.js.

Process manager

На сервере процесс держат через PM2 или systemd:

npm i -g pm2
pm2 start dist/main.js --name notes-api
pm2 save

Переменные окружения

ПеременнаяПримерНазначение
PORT8080Порт HTTP
NODE_ENVproductionРежим runtime
DATABASE_URLсм. 2691Строка подключения к БД

HTTPS и reverse proxy

NestJS слушает HTTP локально; снаружи ставят Nginx или Caddy с TLS. Пример proxy_passNginx — конфиги под задачу.

Production

Для боевого сервера — process manager, секреты в переменных окружения, HTTPS за reverse proxy, логирование и мониторинг. Данные в памяти теряются при перезапуске — нужна БД и бэкапы.


Шаг 8 — конфигурация через @nestjs/config

Секреты и настройки не хранят в коде. Пакет @nestjs/config читает .env:

npm i @nestjs/config

.env:

PORT=3000
NODE_ENV=development

app.module.ts:

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { NotesModule } from './notes/notes.module';

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
NotesModule,
],
})
export class AppModule {}

В main.ts:

import { ConfigService } from '@nestjs/config';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = app.get(ConfigService);
const port = config.get<number>('PORT', 3000);
await app.listen(port);
}
ПеременнаяГде используется
PORTmain.ts — порт HTTP
DATABASE_URLPrisma — строка БД
NODE_ENVлогирование, режим ошибок

Файл .env добавьте в .gitignore. На production переменные задаёт хостинг.


Шаг 9 — e2e-тесты supertest

Nest генерирует каркас тестов. Файл test/notes.e2e-spec.ts:

import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication, ValidationPipe } from '@nestjs/common';
import request from 'supertest';
import { AppModule } from '../src/app.module';

describe('Notes (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();

app = moduleFixture.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});

afterAll(async () => {
await app.close();
});

it('GET /notes returns array', () => {
return request(app.getHttpServer())
.get('/notes')
.expect(200)
.expect((res) => {
expect(Array.isArray(res.body)).toBe(true);
});
});

it('POST /notes creates note', () => {
return request(app.getHttpServer())
.post('/notes')
.send({ text: 'Test note' })
.expect(201)
.expect((res) => {
expect(res.body.text).toBe('Test note');
expect(res.body.id).toBeDefined();
});
});

it('POST /notes rejects empty text', () => {
return request(app.getHttpServer())
.post('/notes')
.send({ text: '' })
.expect(400);
});
});

Запуск:

npm run test:e2e

Разбор:

  • Test.createTestingModule поднимает приложение без реального HTTP-порта.
  • supertest шлёт запросы во in-memory сервер.
  • Тесты фиксируют контракт API — полезно перед рефакторингом на Prisma.

Шаг 10 — Swagger (OpenAPI)

Документация API из декораторов:

npm i @nestjs/swagger

main.ts:

import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

async function bootstrap() {
const app = await NestFactory.create(AppModule);
// ... pipes, cors ...

const config = new DocumentBuilder()
.setTitle('Notes API')
.setDescription('Учебное REST API')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('docs', app, document);

await app.listen(3000);
}

В DTO:

import { ApiProperty } from '@nestjs/swagger';

export class CreateNoteDto {
@ApiProperty({ example: 'Купить хлеб', minLength: 1 })
@IsString()
@MinLength(1)
text!: string;
}

Откройте http://localhost:3000/docs — интерактивная документация и пробные запросы.


Шаг 11 — логирование запросов (Interceptor)

src/common/logging.interceptor.ts:

import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
Logger,
} from '@nestjs/common';
import { Observable, tap } from 'rxjs';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger('HTTP');

intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const req = context.switchToHttp().getRequest();
const { method, url } = req;
const started = Date.now();

return next.handle().pipe(
tap(() => {
const ms = Date.now() - started;
this.logger.log(`${method} ${url} ${ms}ms`);
}),
);
}
}

Подключение в main.ts:

app.useGlobalInterceptors(new LoggingInterceptor());

Аналог middleware-логирования из Express — middleware, но в стиле Nest.


Шаг 12 — глобальный префикс и версионирование

app.setGlobalPrefix('api');

Маршруты станут /api/notes, /api/health. Удобно за reverse proxy, когда статика и API на одном домене.

Версионирование URI:

app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1',
});
@Controller({ path: 'notes', version: '1' })
export class NotesController {}

URL: /api/v1/notes.


Docker — контейнер для production

Dockerfile:

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "dist/main.js"]

.dockerignore:

node_modules
dist
.env
.git

Сборка и запуск:

docker build -t notes-nest .
docker run -p 3000:3000 -e PORT=3000 notes-nest

С Prisma добавьте шаг npx prisma migrate deploy в entrypoint перед node dist/main.js.


Полная структура учебного проекта

notes-nest/
src/
main.ts
app.module.ts
notes/
notes.module.ts
notes.controller.ts
notes.service.ts
dto/
create-note.dto.ts
health/
health.controller.ts
health.module.ts
common/
http-exception.filter.ts
logging.interceptor.ts
prisma/ ← после шага Prisma
prisma.service.ts
test/
notes.e2e-spec.ts
.env
Dockerfile
package.json

Сравнение слоёв Express и NestJS (детально)

ЗадачаExpress (263)NestJS
Маршрут GETapp.get('/notes', handler)@Get() + @Controller('notes')
Валидация bodyручные if или JoiDTO + ValidationPipe
Разделение логикиroutes/notes.jsNotesService + DI
404 для idif (!note) return res.status(404)throw new NotFoundException()
Глобальные middlewareapp.use(fn)Guards, Pipes, Interceptors, Filters
Тесты HTTPsupertest вручную@nestjs/testing + supertest
OpenAPIswagger-jsdoc вручную@nestjs/swagger

NestJS организует код; HTTP по-прежнему тот же протокол — см. HTTP как основа веб-интеграций.


Углублённый troubleshooting

СимптомДеталиРешение
EADDRINUSE :::3000Порт занятnpx kill-port 3000 или смените PORT
Декораторы не работаютtsconfigexperimentalDecorators: true, emitDecoratorMetadata: true
undefined в @Body()Content-TypeКлиент шлёт application/json
Двойная регистрация сервисаДва модуля с одним providerОдин модуль экспортирует, другой импортирует
Swagger пустойDTO без @ApiPropertyДобавьте декораторы или @ApiBody
e2e падает на ValidationPipePipe не подключён в тестеapp.useGlobalPipes(...) в beforeAll
Hot reload не подхватывает .envКэш envПерезапустите start:dev
Prisma в Nest — connection leakНет $disconnectOnModuleDestroy в PrismaService

Дополнительные упражнения

  1. Подключите @nestjs/throttler — лимит 10 запросов в минуту на IP.
  2. Добавьте @Delete(':id') с @HttpCode(204) и e2e-тест на 404.
  3. Сделайте NotesModule глобальным экспортом NotesService для использования в другом модуле.
  4. Напишите unit-тест NotesService с mock-хранилищем без HTTP.
  5. Свяжите фронт Svelte через CORS или прокси Vite.

Расширенный FAQ

Можно ли смешивать Express middleware и Nest?

Да. app.use(cors()) или сторонние middleware подключаются в main.ts до listen.

GraphQL в NestJS?

Отдельный модуль @nestjs/graphql — следующий уровень после REST.

Микросервисы?

Nest поддерживает TCP, Redis, NATS транспорты — для учебного API достаточно монолита.

Как отлаживать в VS Code?

Конфиг launch.json с "runtimeArgs": ["run", "start:debug"] и "console": "integratedTerminal".

NestJS без CLI?

Можно собрать вручную, но nest generate экономит время — см. npm scripts.


Production checklist

ПунктПроверка
Process managerPM2 или systemd
HTTPSNginx/Caddy перед Node
Env secretsНе в git, только хостинг
БДPrisma migrate deploy
Логиstdout + сборщик (Loki, CloudWatch)
HealthGET /health для load balancer
Graceful shutdownapp.enableShutdownHooks()
Rate limitthrottler или nginx
Мониторингuptime, latency, 5xx
// graceful shutdown в main.ts
app.enableShutdownHooks();

При SIGTERM Nest завершит активные запросы перед выходом.


Практикум — ConfigModule и переменные окружения

Production-приложение не хранит секреты в коде. Пакет @nestjs/config читает .env и валидирует значения при старте.

npm i @nestjs/config
import { ConfigModule } from '@nestjs/config';

@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env.local', '.env'] }),
NotesModule,
],
})
export class AppModule {}
@Injectable()
export class NotesService {
constructor(private readonly config: ConfigService) {
this.maxNotes = this.config.get<number>('MAX_NOTES', 1000);
}
}
ПеременнаяDevProduction
PORT3000из PaaS
NODE_ENVdevelopmentproduction
DATABASE_URLSQLite filePostgreSQL URL
API_KEYdev-secretиз vault

Файл .env — в .gitignore. Коммитьте .env.example без секретов.


Практикум — Guards и API-ключ

@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private readonly config: ConfigService) {}

canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest();
const key = req.headers['x-api-key'];
if (key !== this.config.get('API_KEY')) {
throw new UnauthorizedException('Invalid API key');
}
return true;
}
}
@Controller('notes')
@UseGuards(ApiKeyGuard)
export class NotesController {}
curl -H "x-api-key: dev-secret" http://localhost:3000/notes

Практикум — Interceptors и логирование

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
const req = context.switchToHttp().getRequest();
const started = Date.now();
return next.handle().pipe(
tap(() => console.log(`${req.method} ${req.url} ${Date.now() - started}ms`)),
);
}
}

В main.ts: app.useGlobalInterceptors(new LoggingInterceptor());


Практикум — Swagger (OpenAPI)

npm i @nestjs/swagger
const config = new DocumentBuilder()
.setTitle('Notes API')
.setVersion('1.0')
.addApiKey({ type: 'apiKey', name: 'x-api-key', in: 'header' }, 'api-key')
.build();
SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config));

Откройте http://localhost:3000/docs.


Практикум — e2e-тесты

test/notes.e2e-spec.ts:

describe('Notes (e2e)', () => {
let app: INestApplication;

beforeAll(async () => {
const module = await Test.createTestingModule({ imports: [AppModule] }).compile();
app = module.createNestApplication();
app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
await app.init();
});

it('POST /notes creates note', () =>
request(app.getHttpServer())
.post('/notes')
.send({ text: 'E2E' })
.expect(201));
});
npm run test:e2e

Практикум — PATCH и UpdateNoteDto

export class UpdateNoteDto {
@IsOptional()
@IsString()
@MinLength(1)
text?: string;
}

@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() dto: UpdateNoteDto) {
return this.notes.update(id, dto.text!);
}

Практикум — Repository pattern

export interface NotesRepository {
findAll(): Promise<Note[]>;
create(text: string): Promise<Note>;
}

@Module({
providers: [
NotesService,
{ provide: 'NotesRepository', useClass: InMemoryNotesRepository },
],
})
export class NotesModule {}

Позже замените на Prisma — 2691.


Практикум — глобальный префикс

app.setGlobalPrefix('api');

URL: GET /api/notes.


Практикум — Fastify adapter

npm i @nestjs/platform-fastify
const app = await NestFactory.create(AppModule, new FastifyAdapter());

Расширенные упражнения

  1. @nestjs/throttler — 10 req/min на IP.
  2. Unit-тест NotesService с mock repository.
  3. helmet для security headers.
  4. @nestjs/terminus health check.
  5. Pagination GET /notes?page=1&limit=10.
  6. Экспорт openapi.json при build.
  7. Graceful shutdown через enableShutdownHooks().

Расширенный FAQ

Monorepo Nest + React?

Nx или Turborepo: apps/api, apps/web, packages/shared.

GraphQL?

@nestjs/graphql — после освоения REST.

Graceful shutdown?

app.enableShutdownHooks();

JSON-логи?

nestjs-pino или Winston.

Microservices?

@nestjs/microservices — TCP, Redis, Kafka. Начните с monolith.

Nested DTO?

@ValidateNested() + @Type(() => ChildDto).

Как дебажить в VS Code?

Launch config с ts-node/register, breakpoint в controller.

OpenAPI типы для фронта?

openapi-typescript из /docs-json.

Rate limit на nginx?

limit_req_zone — дублирует throttler на edge.

Session auth?

@nestjs/passport + express-session или JWT.


Production — расширенный чек-лист

ОбластьДействие
SecretsVault, не .env в Docker-образе
HTTPSTLS на reverse proxy
Health/health + /health/ready для k8s
MetricsPrometheus или APM
LoggingJSON + correlation id
DockerMulti-stage, non-root user
CIlint, unit, e2e, audit

Dockerfile

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci && COPY . . && npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/main.js"]

docker-compose

services:
api:
build: .
ports: ["3000:3000"]
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/notes
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres

Сценарий полного дня

ВремяЗадача
09:00nest new, NotesModule
10:00DTO + ValidationPipe
11:00Prisma — 2691
13:00Swagger + e2e
14:00Guard + ConfigModule
15:00Docker build
16:00Deploy на PaaS

Отладка в VS Code

{
"configurations": [{
"type": "node",
"request": "launch",
"name": "NestJS",
"runtimeArgs": ["-r", "ts-node/register"],
"args": ["src/main.ts"]
}]
}

Связь с Fullstack

  1. CORS или proxy Vite — 288.
  2. Типы из OpenAPI для Svelte.
  3. См. Fullstack.

Дополнительный практикум — Pipes и Transform

@Injectable()
export class TrimPipe implements PipeTransform {
transform(value: string) {
return typeof value === 'string' ? value.trim() : value;
}
}

@Post()
create(@Body('text', TrimPipe) text: string) {
return this.notes.create(text);
}

Дополнительный практикум — Exception filters

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const res = ctx.getResponse();
const status = exception instanceof HttpException ? exception.getStatus() : 500;
res.status(status).json({
statusCode: status,
timestamp: new Date().toISOString(),
});
}
}

Дополнительный практикум — Custom decorators

export const User = createParamDecorator(
(_data, ctx: ExecutionContext) => ctx.switchToHttp().getRequest().user,
);

Таблица HTTP-кодов для Notes API

КодКогда
200GET успех
201POST создан
204DELETE без тела
400ValidationPipe
401Guard
404NotFoundException
500Необработанная ошибка

Чек-лист перед деплоем

  • npm run build без ошибок
  • e2e зелёные
  • .env не в git
  • DATABASE_URL на хостинге
  • Health endpoint отвечает
  • Swagger отключён или за auth в prod
  • PM2 или k8s restart policy


Второй проход — расширенный практикум (NestJS)

Серия мини-туториалов

Туториал 1 — ConfigModule

Команда или API: npm i @nestjs/config.

Детали: ConfigModule.forRoot({ isGlobal: true }).

// пример шага
console.log('ok');
ШагПроверка
1Выполнить ConfigModule
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 2 — Throttler

Команда или API: npm i @nestjs/throttler.

Детали: ThrottlerModule.forRoot([{ ttl: 60000, limit: 10 }]).

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Throttler
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 3 — Terminus health

Команда или API: npm i @nestjs/terminus.

Детали: HealthCheckService.check([() => db.pingCheck('db')]).

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Terminus health
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 4 — Class serializer

Команда или API: ClassSerializerInterceptor.

Детали: excludeExtraneousValues в DTO.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Class serializer
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 5 — Event emitter

Команда или API: @nestjs/event-emitter.

Детали: emit('note.created', note) в сервисе.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Event emitter
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 6 — Schedule cron

Команда или API: @nestjs/schedule.

Детали: @Cron('0 0 * * *') очистка старых notes.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Schedule cron
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 7 — Caching

Команда или API: @nestjs/cache-manager.

Детали: CACHE_MANAGER inject в серvice.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Caching
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 8 — Serve static

Команда или API: @nestjs/serve-static.

Детали: раздача upload из /public.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Serve static
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 9 — Multer upload

Команда или API: @nestjs/platform-express.

Детали: FileInterceptor('file') на POST.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить Multer upload
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Туториал 10 — WebSockets

Команда или API: @nestjs/websockets.

Детали: Gateway для realtime notes.

// пример шага
console.log('ok');
ШагПроверка
1Выполнить WebSockets
2Перезапустить dev-сервер
3Убедиться в отсутствии ошибок в консоли

Расширенные упражнения (второй проход)

  1. Добавьте soft delete через поле deletedAt в DTO filter.

Подсказка к упражнению 13: Начните с минимального изменения, затем добавьте тест. Тема: Добавьте.

  1. Реализуйте поиск GET /notes?q=текст через Query decorator.

Подсказка к упражнению 14: Начните с минимального изменения, затем добавьте тест. Тема: Реализуйте.

  1. Напишите cron job удаления notes старше 30 дней.

Подсказка к упражнению 15: Начните с минимального изменения, затем добавьте тест. Тема: Напишите.

  1. Подключите Redis cache для findAll.

Подсказка к упражнению 16: Начните с минимального изменения, затем добавьте тест. Тема: Подключите.

  1. Сделайте WebSocket broadcast при create note.

Подсказка к упражнению 17: Начните с минимального изменения, затем добавьте тест. Тема: Сделайте.

  1. Добавьте multipart upload avatar к note (учебный файл).

Подсказка к упражнению 18: Начните с минимального изменения, затем добавьте тест. Тема: Добавьте.

  1. Versioning URI v1 и v2 параллельно.

Подсказка к упражнению 19: Начните с минимального изменения, затем добавьте тест. Тема: Versioning.

  1. Custom decorator @CurrentUser() с mock user.

Подсказка к упражнению 20: Начните с минимального изменения, затем добавьте тест. Тема: Custom.

  1. Integration test с Testcontainers PostgreSQL.

Подсказка к упражнению 21: Начните с минимального изменения, затем добавьте тест. Тема: Integration.

  1. Helmet + compression middleware в main.ts.

Подсказка к упражнению 22: Начните с минимального изменения, затем добавьте тест. Тема: Helmet.


Расширенный FAQ (второй проход)

Нужен ли CQRS?

Для учебного API нет. @nestjs/cqrs при сложных доменах.

Как структурировать monorepo?

apps/api, libs/shared-types, Nx boundaries.

Testing module imports?

Test.createTestingModule({ imports: [NotesModule] }).

Mock Prisma в unit test?

jest.mock или provider useValue с fake client.

Production logging?

Pino JSON, correlation id middleware.

Kubernetes probes?

GET /health/live и /health/ready через Terminus.

OpenAPI client gen?

openapi-generator для TypeScript fetch client.

Validation groups?

groups: ['create'] в ValidationPipe options.

File env per stage?

envFilePath: [.env.${process.env.STAGE}].

Disable Swagger prod?

if (process.env.NODE_ENV !== 'production') setup docs.


Production — дополнительные рекомендации

#ПрактикаЗачем
1Multi-stageMulti-stage Docker, non-root USER node
2SecretsSecrets через platform env, не bake в image
3HorizontalHorizontal scale: stateless API + PostgreSQL
4RateRate limit nginx + Throttler defense in depth
5BackupBackup DATABASE_URL provider snapshots
6Blue-greenBlue-green deploy через два PM2 процесса
7SentrySentry для 5xx stack traces
8DependabotDependabot npm audit weekly

Troubleshooting — расширенная таблица

СимптомВероятная причинаДействие
Сборка падает без текстаКэш или версия NodeОчистить node_modules, lock-файл, переустановить
Тесты flakyПорядок или timingИзолировать example, убрать sleep, добавить wait matchers
Production 502Process не слушает PORTПроверить env PORT и health endpoint
Данные пропали после deployIn-memory store или migrateПодключить БД, migrate deploy
CORS в браузереПрямой URL APIProxy dev или enableCors origin
Медленный первый запросCold start DB poolWarmup health check после deploy
Ошибка подписи iOSCertificate expiredRenew в Developer portal, download profiles
Turbo frame blankId mismatchСверить turbo-frame id в request и response
Prisma client outdatedSchema changednpx prisma generate после migrate
Vite blank prodНеверный base pathПроверить base и URL деплоя

Пошаговый walkthrough — контрольный список

День 1

  1. Шаг 1 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 2

  1. Шаг 1 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 3

  1. Шаг 1 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 4

  1. Шаг 1 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 5

  1. Шаг 1 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 6

  1. Шаг 1 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

День 7

  1. Шаг 1 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 2 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 3 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 4 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
  1. Шаг 5 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check

Чек-лист самопроверки перед сдачей практикума

  • Проект создаётся с нуля по статье без пропусков шагов

  • CRUD или эквивалентный сценарий работает end-to-end

  • Есть обработка ошибок валидации или 404

  • Данные переживают перезапуск там, где это требуется темой

  • Написан минимум один автоматический тест или system check

  • Production-секция прочитана и применена к деплою или Docker

  • FAQ просмотрен — типичные ошибки воспроизведены и исправлены

  • Связанные материалы открыты для следующего шага обучения

Связанные материалы

ТемаМатериал
Node без фреймворкаПервая программа на Node.js
ExpressExpress — middleware, маршруты и ошибки
npmnpm — команды, зависимости и lock-файлы
FullstackFullstack на JavaScript — API и фронтенд
TypeScriptПервая программа на TypeScript
PrismaPrisma ORM — первая программа
DrizzleDrizzle ORM — первая программа
Основа по протоколу

Базовый разбор HTTP и HTTPS — HTTP как основа веб-интеграций.


Содержание