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. Если у вас есть на примете такие же мощные фреймворки, пожалуйста, поделитесь. Будет очень интересно попробовать и сравнить.
Комментарии (13)
Suvitruf
30.07.2022 18:06+2NestJS прекрасный фреймворк под Node.js, вдохновлённый серьёзными фреймворками Spring, ASP.NET Core, Simfony.
Откуда это? Сами разработчики писали, что они вдохновлялись Ангуляром ????разработка превращается в постоянный ад из-за постоянно неправильно настроенных модулей, расстановки их зависимостей
Но ведь вы тут сами пишете, что проблема именно в неправильной настройке модулей.hello_my_name_is_dany Автор
30.07.2022 23:29+3Откуда это? Сами разработчики писали, что они вдохновлялись Ангуляром ????
Да, модули они явно взяли из Angular 2 (поэтому в статье так много отсылок к нему), что стала "вишенкой" этого фреймворка. Но остальные концепции явно из других фреймворков, например, контроллеры с декораторами - аналог контроллера с аннотациями (JavaEE/Spring) или с атрибутами (ASP.NET).
Но ведь вы тут сами пишете, что проблема именно в неправильной настройке модулей.
В том-то и проблема, что такая абстракция, как модули, может эффективно использоваться только в конкретных кейсах определённой архитектуры проекта, но в большинстве случаев они только замедляют и усложняют разработку, не привнося явных плюсов. Даже разделение проекта на npm-пакеты, если нужна модульность, проще, гибче и позволяет даже несильно зависеть от самого фреймворка.
funca
31.07.2022 10:03+3По сравнению с другими языками, где интерфейсы это first-class citizens и DI элегантно пользуются этой особенностью, nest.js вынужден подстраиваться под специфику TS/JS, где ни каких интерфейсов в рантайме не существует. Отсюда кастыли с декоратором Inject и токенами. Это добавляет лишний шум в код, но лучшего пока не придумали.
Модули nest.js описывают жизненный цикл объекта в рантайме - когда и как создавать - чего нет в обычных es6 модулях или npm пакетах. Наличие циклических зависимостей говорит о проблеме в декомпозиции.
Sharku
31.07.2022 04:21Не рассматривали в качестве альтернативы adonisjs.com ?
hello_my_name_is_dany Автор
31.07.2022 04:23Не рассматривали, так как у нас многие сервисы общаются по отличным от HTTP видам транспорта
DonVietnam
31.07.2022 17:17Не совсем понял смысл этой статьи, хотя я и не использую Nest на постоянной основе и с ее внутренними проблемами не знаком. Как по мне Nest так хорошо заходит в сообществе, потому что ему нет альтернатив на данный момент, хотя то, что предлагает этот Фреймворк, а именно обмазывание декораторами, которые могут быть со временем вытеснены стандартными, если те будут приняты, и сомнительными инъекциями зависимостей, меня отталкивает от его использования. По-моему скромному мнению Фреймворк, использующий чистую архитектуру, можно написать и без сомнительных конструкций, не являющихся частью стандарта языка.
dhawkmoon
31.07.2022 19:22А в чем сомнительность DI? Ну и декораторы здесь от type script, хотя все же мне тоже «обмазывание» декораторами не заходит
dopusteam
Чтобы использовать сервис из модуля, разве недостаточно просто подключить модуль, в котором этот сервис объявлен?
hello_my_name_is_dany Автор
Если у сервиса нет зависимостей, то так теоретически можно. А так придётся все зависимости в providers добавлять и тогда уж проще будет сделать только один модуль на весь проект
Поэтому проще сделать export сервиса и импортировать модуль, если делим на модули всё
dopusteam
Т.е. тут не как в ангуляре? Там модуль внутри себя содержит все необходимое, и достаточно подключить модуль, он внутри потянет сам все необходимое.
Вот у меня есть модуль А, который зависит от модулей Б и С и который экспортит сервис А. Могу я подключить в своем приложении тупо модуль А и из него юзать сервис А?
johny_cat
Да, можете. Похоже, что автор статьи что-то напутал либо использовал модули некорректно.
Такое может случиться разве что если вы будете расширять например клас одного сервиса от другого. У меня была подобная ситуация, но я сразу понял что делаю что-то не так и исправил этот момент.
hello_my_name_is_dany Автор
Да и в правду напутал, просто неправильно прочитал комментарий. Подумал, что просто импортировать сервис.
Чтобы использовать сервис надо импортировать модуль, который его экспортирует, в модуль сервиса, к которому мы хотим подключить зависимость от сервиса, как я и писал в статье.
Собственно при увеличении проекта модули зависят друг от друга, что довольно трудно контролировать (те же циклические зависимости, благо Nest помогает немного с этим). А если делать модули больше, например, либо по слоям архитектуры, либо просто один большой модуль на проект, то толку от модулей нет.
Suvitruf