Разработка бэкенда сайта на Node.js (NestJS)
NestJS — это не просто фреймворк, это архитектурное решение. Он навязывает структуру, которую команда иначе изобретала бы сама: модули, провайдеры, dependency injection, декораторы. Для сложных проектов с несколькими разработчиками это ценность. Для небольшого API в одиночку — избыточность.
Выбор NestJS оправдан при: монорепо с несколькими приложениями, микросервисной архитектуре, команде от трёх разработчиков, долгосрочном проекте с меняющейся командой.
Структура модулей
Каждый функциональный блок — отдельный модуль. Модуль объявляет, что предоставляет наружу и что импортирует:
// users/users.module.ts
@Module({
imports: [TypeOrmModule.forFeature([User]), JwtModule],
controllers: [UsersController],
providers: [UsersService, UsersRepository],
exports: [UsersService]
})
export class UsersModule {}
// app.module.ts
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => ({
type: 'postgres',
url: config.get('DATABASE_URL'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
migrations: [__dirname + '/migrations/*{.ts,.js}'],
synchronize: false
}),
inject: [ConfigService]
}),
UsersModule,
ProductsModule,
AuthModule,
OrdersModule,
]
})
export class AppModule {}
Контроллеры и валидация
Контроллер отвечает только за HTTP-слой: маршруты, заголовки, статус-коды. Логика — в сервисах.
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get()
@Roles('admin')
findAll(@Query() paginationDto: PaginationDto): Promise<PaginatedResult<User>> {
return this.usersService.findAll(paginationDto)
}
@Get(':id')
@ApiParam({ name: 'id', type: 'number' })
async findOne(@Param('id', ParseIntPipe) id: number): Promise<User> {
const user = await this.usersService.findById(id)
if (!user) throw new NotFoundException(`User ${id} not found`)
return user
}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto)
}
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto
): Promise<User> {
return this.usersService.update(id, updateUserDto)
}
}
Валидация через class-validator + ValidationPipe:
export class CreateUserDto {
@IsEmail()
@Transform(({ value }) => value.toLowerCase().trim())
email: string
@IsString()
@MinLength(8)
@Matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, {
message: 'Пароль должен содержать заглавные, строчные буквы и цифры'
})
password: string
@IsOptional()
@IsString()
@MaxLength(100)
name?: string
}
ValidationPipe с whitelist: true автоматически обрезает все поля, не объявленные в DTO — это защита от массового присвоения.
ORM: TypeORM vs Prisma vs MikroORM
NestJS официально поддерживает TypeORM, Prisma, MikroORM, Mongoose. Сравнение для PostgreSQL-проектов:
| TypeORM | Prisma | MikroORM | |
|---|---|---|---|
| Migrations | Встроенные | Встроенные | Встроенные |
| Типизация | Через декораторы | Генерируется из схемы | Unit of Work паттерн |
| Слабые стороны | Медленный при сложных JOIN | Генерация типов, доп. зависимость | Кривая обучения |
| Production-ready | Да | Да | Да |
TypeORM entity:
@Entity('products')
@Index(['slug'], { unique: true })
export class Product {
@PrimaryGeneratedColumn()
id: number
@Column({ length: 255 })
name: string
@Column({ unique: true, length: 255 })
slug: string
@Column('decimal', { precision: 10, scale: 2 })
price: number
@Column({ type: 'jsonb', nullable: true })
attributes: Record<string, unknown>
@ManyToOne(() => Category, (category) => category.products, { onDelete: 'SET NULL' })
@JoinColumn({ name: 'category_id' })
category: Category
@CreateDateColumn({ name: 'created_at' })
createdAt: Date
@UpdateDateColumn({ name: 'updated_at' })
updatedAt: Date
}
Аутентификация
Паттерн: JWT access token (15 мин) + refresh token (30 дней) в httpOnly cookie.
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
private configService: ConfigService
) {}
async login(user: User): Promise<{ accessToken: string; refreshToken: string }> {
const payload = { sub: user.id, email: user.email, role: user.role }
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload, {
secret: this.configService.get('JWT_SECRET'),
expiresIn: '15m'
}),
this.jwtService.signAsync({ sub: user.id }, {
secret: this.configService.get('JWT_REFRESH_SECRET'),
expiresIn: '30d'
})
])
await this.usersService.saveRefreshToken(user.id, refreshToken)
return { accessToken, refreshToken }
}
}
Очереди и фоновые задачи
@nestjs/bull (Bull + Redis) для async-задач:
@Processor('email')
export class EmailProcessor {
@Process('welcome')
async sendWelcomeEmail(job: Job<{ userId: number }>): Promise<void> {
const user = await this.usersService.findById(job.data.userId)
await this.mailerService.send({
to: user.email,
subject: 'Добро пожаловать',
template: 'welcome',
context: { name: user.name }
})
}
@Process('order-confirmation')
@OnQueueFailed()
async handleFailure(job: Job, error: Error): Promise<void> {
this.logger.error(`Job ${job.id} failed: ${error.message}`)
// алерт в Sentry/Telegram
}
}
Микросервисы
NestJS нативно поддерживает микросервисную архитектуру через транспортные слои:
// main.ts для микросервиса
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.RMQ,
options: {
urls: [process.env.RABBITMQ_URL],
queue: 'orders_queue',
queueOptions: { durable: true }
}
})
// Обработчик сообщений
@MessagePattern('create_order')
async createOrder(@Payload() data: CreateOrderDto, @Ctx() context: RmqContext) {
const channel = context.getChannelRef()
const originalMsg = context.getMessage()
try {
const order = await this.ordersService.create(data)
channel.ack(originalMsg)
return order
} catch (err) {
channel.nack(originalMsg, false, false)
throw err
}
}
OpenAPI документация
Swagger генерируется из декораторов и DTO без дополнительных схем:
// main.ts
const config = new DocumentBuilder()
.setTitle('API')
.setVersion('1.0')
.addBearerAuth()
.build()
const document = SwaggerModule.createDocument(app, config, {
operationIdFactory: (controllerKey, methodKey) => methodKey
})
SwaggerModule.setup('api/docs', app, document)
Тестирование
NestJS включает тестовые утилиты из коробки:
describe('UsersService', () => {
let service: UsersService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: getRepositoryToken(User), useValue: mockRepository }
]
}).compile()
service = module.get<UsersService>(UsersService)
})
it('should throw NotFoundException when user not found', async () => {
mockRepository.findOne.mockResolvedValue(null)
await expect(service.findById(999)).rejects.toThrow(NotFoundException)
})
})
Сроки разработки
NestJS-проект требует больше времени на старте из-за структуры, но экономит на поддержке:
- Архитектура и проектирование модулей — 1 неделя: схема БД, модули, DTO, API-контракт
- Базовый каркас + auth — 1–1,5 недели
- Модули бизнес-логики — 2–4 недели в зависимости от числа сущностей
- Интеграции, очереди, файлы — 1–3 недели
- Тесты (unit + e2e) — 1–2 недели
- Документация Swagger + DevOps — 3–5 дней
Средний корпоративный сайт или веб-приложение: 6–12 недель. Монорепо с несколькими сервисами — от 3 месяцев. NestJS окупает вложения на проектах, которые живут дольше года и меняют команду.







