javascript

Почему я люблю и ненавижу NestJS?

  • воскресенье, 31 июля 2022 г. в 00:35:44
https://habr.com/ru/post/679992/
  • JavaScript
  • Анализ и проектирование систем
  • Node.JS
  • API
  • TypeScript


NestJS прекрасный фреймворк под Node.js, вдохновлённый серьёзными фреймворками Spring, ASP.NET Core, Simfony.

Так что же там внутри прекрасного и ужасного?

Основы

Для тех, кто не знаком с этим фрейморком, расскажу. В первую очередь он рассчитан на TypeScript, но благодаря babel можно делать проекты и на JavaScript. Это потому что основная работа фреймворка завязана на декораторах.  В его основе лежит взятая из Angular 2+ модульность, которая обеспечивает помимо инкапсуляции ещё и dependency injection. NestJs не стоит рассматривать как очередной HTTP Фреймворк. Во-первых, он платформенно-независим, то есть реализацию HTTP-сервера можете сделать сами (сам NestJS предлагает выбрать Express или Fastify). Во-вторых, можно использовать любой другой вид транспорта: gRPC, TCP или опять таки можно написать свой адаптер. Или не использовать API вообще: запланированные задачи (Cron), обработка сообщений (Redis, Kafka, RabbitMQ, NATS). Также он имеет хорошие интеграции с популярными ORM: TypeORM, Sequelize, Prisma, Mongoose. Только разбирая эту кучу интеграций, можно написать цикл статей, их очень много и я далеко не всё перечислил.

Организация кода

Как и говорилось ранее, самое главное в NestJS - это модуль. Модуль представляет собой некую абстрактную единицу, которая инкапсулирует в себе некий функционал. Это может быть ваш API, работа с БД, домены, работа с внешними сервисами, конфигурации и тд. Но самое главное - это контекст, который создаёт модуль. Внутри модуля создаётся контекст DI-контейнера и через него разрешаются зависимости (Injectable services). В принципе, всё то же самое, что и в Angular 2+ за тем исключением, что вместо параметра components у нас параметр controllers, в который можно добавить наши контроллеры для RESTful API, рендеринга шаблонов или обработчиков сообщений, и параметр providers, в который мы добавляем сервисы или провайдеры/фабрики для создания сервисов. 

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

Контроллеры, сервисы, разные обработчики устроены так же, как и, например, в Spring. Создаётся класс, помечается нужным декоратором (Controller, Handler, Injectable). Методы класса тоже помечаются декораторами (Get, Post, Put, Patch, Delete, Message и тд). Зависимости встраиваются через конструктор. В общем всё очень серьезно.

import { Injectable } from '@nestjs/common';
import { User } from './user.entity';

@Injectable()
export class UserService {
  private readonly users: User[] = [];

  findAll(): User[] {
    return this.users;
  }
}
import { Controller, Get } from '@nestjs/common';
import { User } from './user.entity';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
	constructor(
  	private readonly userService: UserService,
  ) {}

  @Get()
  findAll(): User[] {
    return userService.findAll();
  }
}

Так какие же проблемы?

Проблемы в начале и на маленьких проектах не видны. Мы делаем один модуль, в него запихиваем всё и радуемся жизни. Но если у нас много контроллеров, зачем их пихать в один модуль? Правильно, давайте делить по доменам наше приложение на модули. И вот тут NestJS показывает себя во всей красе.

Помните, что модули имеют свой контекст для DI? Да, мало нам npm для зависимостей, теперь тут целые модули NestJS. Всегда нужно помнить, какие модули где и как зависят от других модулей, чтобы можно было встроить один сервис из одного модуля в сервис другого модуля. А ещё модули бывают динамическими и встроить сервисы из него можно только благодаря волшебному декоратору Inject, который далеко не везде предоставляется возможным использовать. В принципе для решения таких проблем придумали глобальные модули, которые один раз импортируются, например, в главном модуле, можно использовать экспортируемые сервисы во всех модулях, но это больше похоже на костыль, чем на нормальное решение.

Ладно, а если мы будем делить модули не по доменам, а по слоям: domain, infrastructure, application, api, workers и т.д.? Мы снова натыкаемся на модули, потому что чтобы инжектировать, например, репозиторий из модуля infrastructure в модуль application, мы должны явно импортировать infrastructure модуль в модуле application, и тем самым мы лишаемся "чистой архитектуры".

import { Module } from '@nestjs/common';
import { UserRepository } from './repositories/user.repository';

@Module({
  providers: [UserRepository],
  exports: [UserRepository],
})
export class InfrastructureModule {}
import { Module } from '@nestjs/common';
import { InfrastructureModule } from '../infrastructure/infrastructure.module';
import { UserService } from './services/user.service';

@Module({
  imports: [InfrastructureModule],
  providers: [UserService],
  exports: [UserService],
})
export class ApplicationModule {}

Итоги

NestJS предоставляет мощные инструменты для Enterprise-разработки. Модули предоставляют мощный инструмент для инкапсуляции вашего функционала, но стоит ли оно того, когда разработка превращается в постоянный ад из-за постоянно неправильно настроенных модулей, расстановки их зависимостей? Вопрос остаётся открытым, но лично я за это ненавижу NestJS. Ведь тот же Spring и ASP.NET Core живут и без этих модулей, но предоставляют всё же больше возможностей для “чистой” разработки. 

P.S. Если у вас есть на примете такие же мощные фреймворки, пожалуйста, поделитесь. Будет очень интересно попробовать и сравнить.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Используете ли вы Nest.js в production?
59.46% Да 22
40.54% Нет 15
Проголосовали 37 пользователей. Воздержались 8 пользователей.