Первая программа на 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 | Каркас проекта |
| 2 | AppModule, AppController | Точка входа и маршрут |
| 3 | NotesService + DI | Бизнес-логика отдельно от HTTP |
| 4 | DTO и ValidationPipe | Валидация тела запроса |
| 5 | CRUD /notes | Учебное REST API |
| 6 | Guards и фильтры ошибок | Единый формат ответов |
| 7 | Сборка и деплой | Production-ready сервис |
| Материал | Зачем |
|---|---|
| Первая программа на Node.js | npm, Express, REST "Заметки" |
| Express — middleware | Router, CORS, errorHandler |
| npm — команды и lock-файлы | install, scripts, audit |
| TypeScript — первая программа | типы, интерфейсы, strict |
| Prisma ORM — первая программа | замена in-memory store |
| Fullstack на JavaScript | CORS, прокси, два порта |
Навигация по блоку Node.js
- Предыдущий шаг: Первая программа на Node.js → Express — middleware, маршруты и ошибки
- Вы здесь: Первая программа на NestJS
- База данных: Prisma ORM — первая программа, Drizzle ORM — первая программа
- npm и структура: npm — команды, зависимости и lock-файлы
Команды удобно выполнять во вкладке Терминал 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.
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.ts | HTTP-слой |
*.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 | /notes | 200, массив |
| GET | /notes/1 | 200, объект |
| POST | /notes | 201, созданная заметка |
| DELETE | /notes/1 | 204, пустое тело |
| GET | /notes/999 | 404, JSON с сообщением |
Шаг 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), вот соответствие слоёв:
| Express | NestJS |
|---|---|
app.get('/notes', ...) | @Get() в NotesController |
express.json() | Встроено; DTO + ValidationPipe |
Массив notes в server.js | NotesService с приватным массивом |
app.use(errorHandler) | @Catch() фильтры |
Router в отдельном файле | @Module + @Controller |
NestJS и Express — когда что выбирать
| Сценарий | NestJS | Express вручную |
|---|---|---|
| Команда знает Angular или Spring | привычные паттерны | проще старт, без DI |
| Крупный REST или GraphQL backend | модули, guards, pipes | нужна дисциплина в структуре |
| Микроскрипт на 3 маршрута | избыточен | подходит |
| Строгая TypeScript-архитектура | из коробки | возможна, но вручную |
| Нужен Fastify вместо Express | NestFactory.create(..., { adapter }) | отдельная настройка |
NestJS не заменяет понимание HTTP и Express — он организует код поверх них. Middleware из 263 работает и в Nest через app.use().
Упражнения
- Добавьте
PATCH /notes/:idдля редактирования текста. СоздайтеUpdateNoteDtoс@IsOptional()и@MinLength(1). - Вынесите
NotesServiceв отдельныйNotesRepository(интерфейс + in-memory реализация) — подготовка к Prisma. - Напишите e2e-тест
test/notes.e2e-spec.tsчерез@nestjs/testingиsupertest. - Подключите Prisma ORM — первая программа вместо массива в
NotesService. - Добавьте глобальный префикс
app.setGlobalPrefix('api')— все маршруты станут/api/notes.
Частые ошибки и troubleshooting
| Симптом | Причина | Что сделать |
|---|---|---|
Nest can't resolve dependencies of NotesController | Сервис не в providers модуля | Добавьте NotesService в @Module({ providers: [...] }) |
404 на /notes | Модуль не импортирован в AppModule | imports: [NotesModule] |
| Validation не срабатывает | Нет ValidationPipe в main.ts | app.useGlobalPipes(new ValidationPipe(...)) |
Cannot find module '@nestjs/common' | Нет npm install | npm install в корне проекта |
| Порт занят | Другой процесс на 3000 | PORT=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
Переменные окружения
| Переменная | Пример | Назначение |
|---|---|---|
PORT | 8080 | Порт HTTP |
NODE_ENV | production | Режим runtime |
DATABASE_URL | см. 2691 | Строка подключения к БД |
HTTPS и reverse proxy
NestJS слушает HTTP локально; снаружи ставят Nginx или Caddy с TLS. Пример proxy_pass — Nginx — конфиги под задачу.
Для боевого сервера — 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);
}
| Переменная | Где используется |
|---|---|
PORT | main.ts — порт HTTP |
DATABASE_URL | Prisma — строка БД |
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 /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 |
|---|---|---|
| Маршрут GET | app.get('/notes', handler) | @Get() + @Controller('notes') |
| Валидация body | ручные if или Joi | DTO + ValidationPipe |
| Разделение логики | routes/notes.js | NotesService + DI |
| 404 для id | if (!note) return res.status(404) | throw new NotFoundException() |
| Глобальные middleware | app.use(fn) | Guards, Pipes, Interceptors, Filters |
| Тесты HTTP | supertest вручную | @nestjs/testing + supertest |
| OpenAPI | swagger-jsdoc вручную | @nestjs/swagger |
NestJS организует код; HTTP по-прежнему тот же протокол — см. HTTP как основа веб-интеграций.
Углублённый troubleshooting
| Симптом | Детали | Решение |
|---|---|---|
EADDRINUSE :::3000 | Порт занят | npx kill-port 3000 или смените PORT |
| Декораторы не работают | tsconfig | experimentalDecorators: true, emitDecoratorMetadata: true |
undefined в @Body() | Content-Type | Клиент шлёт application/json |
| Двойная регистрация сервиса | Два модуля с одним provider | Один модуль экспортирует, другой импортирует |
| Swagger пустой | DTO без @ApiProperty | Добавьте декораторы или @ApiBody |
| e2e падает на ValidationPipe | Pipe не подключён в тесте | app.useGlobalPipes(...) в beforeAll |
Hot reload не подхватывает .env | Кэш env | Перезапустите start:dev |
| Prisma в Nest — connection leak | Нет $disconnect | OnModuleDestroy в PrismaService |
Дополнительные упражнения
- Подключите
@nestjs/throttler— лимит 10 запросов в минуту на IP. - Добавьте
@Delete(':id')с@HttpCode(204)и e2e-тест на 404. - Сделайте
NotesModuleглобальным экспортомNotesServiceдля использования в другом модуле. - Напишите unit-тест
NotesServiceс mock-хранилищем без HTTP. - Свяжите фронт 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 manager | PM2 или systemd |
| HTTPS | Nginx/Caddy перед Node |
| Env secrets | Не в git, только хостинг |
| БД | Prisma migrate deploy |
| Логи | stdout + сборщик (Loki, CloudWatch) |
| Health | GET /health для load balancer |
| Graceful shutdown | app.enableShutdownHooks() |
| Rate limit | throttler или 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);
}
}
| Переменная | Dev | Production |
|---|---|---|
PORT | 3000 | из PaaS |
NODE_ENV | development | production |
DATABASE_URL | SQLite file | PostgreSQL URL |
API_KEY | dev-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());
Расширенные упражнения
@nestjs/throttler— 10 req/min на IP.- Unit-тест
NotesServiceс mock repository. helmetдля security headers.@nestjs/terminushealth check.- Pagination
GET /notes?page=1&limit=10. - Экспорт
openapi.jsonпри build. - 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 — расширенный чек-лист
| Область | Действие |
|---|---|
| Secrets | Vault, не .env в Docker-образе |
| HTTPS | TLS на reverse proxy |
| Health | /health + /health/ready для k8s |
| Metrics | Prometheus или APM |
| Logging | JSON + correlation id |
| Docker | Multi-stage, non-root user |
| CI | lint, 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 /app/dist ./dist
COPY /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:00 | nest new, NotesModule |
| 10:00 | DTO + ValidationPipe |
| 11:00 | Prisma — 2691 |
| 13:00 | Swagger + e2e |
| 14:00 | Guard + ConfigModule |
| 15:00 | Docker build |
| 16:00 | Deploy на PaaS |
Отладка в VS Code
{
"configurations": [{
"type": "node",
"request": "launch",
"name": "NestJS",
"runtimeArgs": ["-r", "ts-node/register"],
"args": ["src/main.ts"]
}]
}
Связь с 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
| Код | Когда |
|---|---|
| 200 | GET успех |
| 201 | POST создан |
| 204 | DELETE без тела |
| 400 | ValidationPipe |
| 401 | Guard |
| 404 | NotFoundException |
| 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 | Убедиться в отсутствии ошибок в консоли |
Расширенные упражнения (второй проход)
- Добавьте soft delete через поле deletedAt в DTO filter.
Подсказка к упражнению 13: Начните с минимального изменения, затем добавьте тест. Тема: Добавьте.
- Реализуйте поиск GET /notes?q=текст через Query decorator.
Подсказка к упражнению 14: Начните с минимального изменения, затем добавьте тест. Тема: Реализуйте.
- Напишите cron job удаления notes старше 30 дней.
Подсказка к упражнению 15: Начните с минимального изменения, затем добавьте тест. Тема: Напишите.
- Подключите Redis cache для findAll.
Подсказка к упражнению 16: Начните с минимального изменения, затем добавьте тест. Тема: Подключите.
- Сделайте WebSocket broadcast при create note.
Подсказка к упражнению 17: Начните с минимального изменения, затем добавьте тест. Тема: Сделайте.
- Добавьте multipart upload avatar к note (учебный файл).
Подсказка к упражнению 18: Начните с минимального изменения, затем добавьте тест. Тема: Добавьте.
- Versioning URI v1 и v2 параллельно.
Подсказка к упражнению 19: Начните с минимального изменения, затем добавьте тест. Тема: Versioning.
- Custom decorator @CurrentUser() с mock user.
Подсказка к упражнению 20: Начните с минимального изменения, затем добавьте тест. Тема: Custom.
- Integration test с Testcontainers PostgreSQL.
Подсказка к упражнению 21: Начните с минимального изменения, затем добавьте тест. Тема: Integration.
- 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 — дополнительные рекомендации
| # | Практика | Зачем |
|---|---|---|
| 1 | Multi-stage | Multi-stage Docker, non-root USER node |
| 2 | Secrets | Secrets через platform env, не bake в image |
| 3 | Horizontal | Horizontal scale: stateless API + PostgreSQL |
| 4 | Rate | Rate limit nginx + Throttler defense in depth |
| 5 | Backup | Backup DATABASE_URL provider snapshots |
| 6 | Blue-green | Blue-green deploy через два PM2 процесса |
| 7 | Sentry | Sentry для 5xx stack traces |
| 8 | Dependabot | Dependabot npm audit weekly |
Troubleshooting — расширенная таблица
| Симптом | Вероятная причина | Действие |
|---|---|---|
| Сборка падает без текста | Кэш или версия Node | Очистить node_modules, lock-файл, переустановить |
| Тесты flaky | Порядок или timing | Изолировать example, убрать sleep, добавить wait matchers |
| Production 502 | Process не слушает PORT | Проверить env PORT и health endpoint |
| Данные пропали после deploy | In-memory store или migrate | Подключить БД, migrate deploy |
| CORS в браузере | Прямой URL API | Proxy dev или enableCors origin |
| Медленный первый запрос | Cold start DB pool | Warmup health check после deploy |
| Ошибка подписи iOS | Certificate expired | Renew в Developer portal, download profiles |
| Turbo frame blank | Id mismatch | Сверить turbo-frame id в request и response |
| Prisma client outdated | Schema changed | npx prisma generate после migrate |
| Vite blank prod | Неверный base path | Проверить base и URL деплоя |
Пошаговый walkthrough — контрольный список
День 1
- Шаг 1 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 1: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 2
- Шаг 1 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 2: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 3
- Шаг 1 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 3: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 4
- Шаг 1 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 4: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 5
- Шаг 1 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 5: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 6
- Шаг 1 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 5 дня 6: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
День 7
- Шаг 1 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 2 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 3 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 4 дня 7: закрепить часть стека NestJS. Запишите результат в README проекта.
# checkpoint
npm test || bundle exec rspec || echo manual check
- Шаг 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 |
| Express | Express — middleware, маршруты и ошибки |
| npm | npm — команды, зависимости и lock-файлы |
| Fullstack | Fullstack на JavaScript — API и фронтенд |
| TypeScript | Первая программа на TypeScript |
| Prisma | Prisma ORM — первая программа |
| Drizzle | Drizzle ORM — первая программа |
Базовый разбор HTTP и HTTPS — HTTP как основа веб-интеграций.