Skip to main content

Що таке NestJS і які проблеми він вирішує?

NestJS - це Node.js фреймворк на базі TypeScript, який додає Angular-подібну структуру (модулі, декоратори, впровадження залежностей) поверх Express для побудови масштабованих серверних додатків.

Теорія

TL;DR

  • NestJS схожий на перетворення розкиданих Express-файлів на пронумеровані пакети: кожна фіча отримує власний модуль (контролери + сервіси + провайдери), з'єднані автоматично
  • Головна різниця: Express не нав'язує жодних правил; NestJS задає чітку структуру через декоратори та модулі
  • Під капотом все одно запускається Express (або Fastify), тобто NestJS це шар зверху, не заміна
  • Правило вибору: прототип на тиждень на самоті? Express. Командний проект або API з 5+ ендпоінтів? NestJS

Швидкий приклад

Той самий GET /users ендпоінт в обох підходах:

typescript
// Express: працює, але куди це класти коли проект росте? const express = require('express'); const app = express(); app.get('/users', (req, res) => res.json([{ id: 1, name: 'Alice' }])); app.listen(3000); // NestJS: автоматично знаходиться через декоратори, одне чітке місце на фічу import { Controller, Get } from '@nestjs/common'; @Controller('users') export class UsersController { @Get() getUsers() { return [{ id: 1, name: 'Alice' }]; // GET /users → [{"id":1,"name":"Alice"}] } } // Запуск: nest start (модулі скануються автоматично)

Для трьох маршрутів Express цілком підходить. На тридцяти маршрутах з п'ятьма розробниками і без спільних домовленостей починається хаос.

Ключова різниця

Express дає порожній аркуш. Маршрути пишеш де хочеш, моделі імпортуєш вручну, правил немає. NestJS спочатку проводить лінії: кожна фіча живе в модулі (як NgModule в Angular), контролери обробляють HTTP через декоратори (@Get, @Post), а сервіси містять логіку з автоматично впровадженими залежностями (dependency injection). Це позбавляє від класичного командного питання "де взагалі живе цей маршрут?" на кожному code review в неструктурованих Express-проектах.

Коли використовувати

  • Прототип на самоті, менше тижня: Express (немає кривої навчання, швидший старт)
  • Командний проект або API з 5+ ендпоінтів: NestJS (структура запобігає конфліктам ще до того як вони виникають)
  • Мікросервіси або WebSockets: NestJS (вбудована підтримка через @nestjs/microservices і @nestjs/websockets)
  • Висока навантаженість, 10k+ RPS: NestJS з Fastify-адаптером (приблизно вдвічі більша пропускна здатність порівняно зі стандартним Express)

Порівняльна таблиця

АспектExpressNestJSFastifyKoa
TypeScriptОпціональноВбудованийОпціональноОпціонально
СтруктураВідсутняМодулі задають розміщенняВідсутняМінімальна
DI-контейнерВручнуВбудованийВручнуВручну
ДекораториНіТакНіНі
CLIНіТак (nest CLI)НіНі
Крива навчання~1 година~1 день~1 година~2 години
Найкраще дляСкрипти, маленькі додаткиКомандні API, великі бекендиШвидкісні APIЛегковагий middleware

Як NestJS запускається

NestJS сканує main.ts, зчитує TypeScript-декоратори (@Module, @Controller) через пакет reflect-metadata, будує граф залежностей і створює провайдери в правильному порядку. Потім реєструє Express-обробники маршрутів з декорованих методів. Все це відбувається до першого запиту.

typescript
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // сканує декоратори, будує DI-граф await app.listen(3000); } bootstrap();

Команди, які я бачив переходять з чистого Express на NestJS, кажуть одне і те ж після першого місяця: вони менше сперечаються про те куди класти новий код.

Типові помилки

Звалити всі маршрути в app.controller.ts:

typescript
// Неправильно: один контролер стає всім додатком @Controller() export class AppController { @Get('users') getUsers() { ... } @Get('products') getProducts() { ... } @Post('orders') createOrder() { ... } // через 500 рядків нічого не можна тестувати окремо }

Модулі існують саме щоб уникнути цього. Один контролер на один домен. users.controller.ts обробляє тільки users.

Ручне створення сервісу замість DI:

typescript
// Неправильно constructor() { this.service = new UsersService(); // обходить DI-контейнер } // Правильно constructor(private service: UsersService) {} // впроваджується автоматично як singleton

Ручний new ламає автоматичне мокування в тестах і обходить singleton-scoping. Це найпоширеніша помилка NestJS на Stack Overflow.

Забути зареєструвати провайдер у модулі:

typescript
// Неправильно: сервіс не вказаний у providers @Module({ controllers: [UsersController], // providers: [UsersService] <- відсутнє }) export class UsersModule {} // Результат: впровадження не спрацьовує, 404 у продакшені без чіткої помилки

NestJS ігнорує будь-який провайдер, якого немає у providers. Завжди реєструй там свої сервіси.

Де NestJS використовується

  • Adidas: модулі для замовлень та інвентарю, GraphQL через @nestjs/graphql
  • Autodesk: мікросервіси з інтеграцією Kafka через @nestjs/microservices
  • Auth-потоки: @nestjs/passport + @nestjs/jwt є стандартною комбінацією в більшості NestJS-проектів
  • WebSocket-чати: @nestjs/websockets з Socket.io gateway

Питання на співбесіді

Q: Що відбувається при запуску NestJS? Що робить NestFactory.create()?
A: Сканує AppModule, зчитує всі @Module-декоратори через reflect-metadata, будує граф залежностей і створює провайдери в порядку залежностей. Потім реєструє Express-обробники маршрутів. app.listen(3000) прив'язує HTTP-сервер останнім.

Q: В чому різниця між провайдером і контролером?
A: Контролери обробляють HTTP: отримують запити і повертають відповіді. Провайдери (з @Injectable()) містять бізнес-логіку і можуть впроваджуватись де завгодно. Контролер використовує провайдер; провайдер не повинен знати про HTTP.

Q: Як переключитись з Express на Fastify?
A: Передай FastifyAdapter у NestFactory.create(AppModule, new FastifyAdapter()). Всі декоратори залишаються точно такими самими. Fastify дає приблизно вдвічі більшу пропускну здатність при великому навантаженні.

Q: Поясни порядок виконання guards, pipes та interceptors.
A: Спочатку Guard (перевірка автентифікації). Потім Pipe (валідація та трансформація вхідних даних). Interceptor обгортає виклик контролера, спрацьовує до і після. Exception filter перехоплює все що кидає помилку. Цей порядок зафіксований у NestJS.

Q: Як би ти побудував NestJS-бекенд для мільйона запитів на день?
A: Окремі модулі на кожен домен, Redis-кешування через @nestjs/cache-manager, health checks через @nestjs/terminus, transient scope для сервісів зі станом і горизонтальне масштабування через Kubernetes з мікросервісами, що спілкуються через черги повідомлень.

Приклади

Базовий: модуль, контролер і сервіс

typescript
// users.module.ts import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; @Module({ controllers: [UsersController], providers: [UsersService], // NestJS впроваджує це в контролер автоматично }) export class UsersModule {} // users.service.ts import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private users = [{ id: 1, name: 'Bob', email: 'bob@example.com' }]; findAll() { return this.users; } findOne(id: number) { return this.users.find(u => u.id === id); } } // users.controller.ts import { Controller, Get, Param } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private usersService: UsersService) {} // впроваджується автоматично @Get() getUsers() { return this.usersService.findAll(); } // GET /users → всі користувачі @Get(':id') getUser(@Param('id') id: string) { return this.usersService.findOne(+id); // GET /users/1 → один об'єкт користувача } }

UsersService вказаний у providers, тому NestJS створює один singleton-екземпляр і передає його в конструктор контролера. Без new, без ручного підключення.

Середній: валідація вхідних даних через DTO і ValidationPipe

typescript
// create-user.dto.ts import { IsString, IsEmail, MinLength } from 'class-validator'; export class CreateUserDto { @IsString() @MinLength(2) name: string; @IsEmail() email: string; } // users.controller.ts (додано POST ендпоінт) import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common'; import { CreateUserDto } from './dto/create-user.dto'; @Controller('users') export class UsersController { @Post() @UsePipes(new ValidationPipe()) createUser(@Body() dto: CreateUserDto) { // Невалідні дані не доходять до цього рядка return { message: `Created user ${dto.name}` }; } } // POST /users з { "name": "A", "email": "notanemail" } // → 400: ["name must be longer than or equal to 2 characters", "email must be an email"]

ValidationPipe зчитує class-validator декоратори з DTO і відхиляє невалідні дані до того як вони потрапляють у логіку контролера. Без жодних ручних перевірок.

Просунутий: кругова залежність і forwardRef

typescript
// cats.service.ts import { Injectable, forwardRef, Inject } from '@nestjs/common'; import { DogsService } from '../dogs/dogs.service'; @Injectable() export class CatsService { constructor( @Inject(forwardRef(() => DogsService)) private dogsService: DogsService, ) {} meow() { return 'meow'; } } // dogs.service.ts import { Injectable, forwardRef, Inject } from '@nestjs/common'; import { CatsService } from '../cats/cats.service'; @Injectable() export class DogsService { constructor( @Inject(forwardRef(() => CatsService)) private catsService: CatsService, ) {} bark() { return this.catsService.meow() + ' woof'; } } // Без forwardRef з обох боків: // → Error: "Cannot resolve dependency CatsService" при запуску

Кругові залежності зазвичай сигналізують про проблему дизайну, яку варто виправити. Але коли двом сервісам справді потрібна взаємна залежність, forwardRef дозволяє NestJS вирішити її під час виконання замість падіння при старті. Фіксуй як технічний борг і плануй рефакторинг.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?