Если, когда вы смотрите на NEST.js вас гнетёт необъяснимая тоска. Если вы не можете понять воодушевления и радости от использования декораторов. Если рассмотрение очередного NEST-инструмента вызывает лёгкое недоумение — не стесняйтесь, вы не одиноки.
NEST.js – это фреймворк для написания REST серверов под Node.js на языке TypeScript, который потом транспилируется в JavaScript. Он написан поверх библиотеки Express (или Fastify – можно выбрать) и привносит модные концепции – Inversion of Control, Dependency Injection и т. п. в мир JavaScript. Нередко описание этого инструмента сопровождается восторженным настроением. Как мне кажется, эта восторженность несколько преувеличена, сложность излишняя, а чудо-сила отсутствует. Некоторые неудобства вынудили нас отказаться от его использования после нескольких лет разработки.
Disclaimer
Вообще-то оно работает. Код переусложнённый, а после транспиляции запутанный, но, в принципе, работает. Используется TypeScript по умолчанию, и это правильно. В целом похоже на методологию Domain Driven Design (DDD) – и это тоже правильно. Внешне и по смыслу код бывает очень похож на написанный на фреймворке Spring на Java, поэтому если вы джавист и по необъяснимой причине что-то должны писать на Ноде, то это удобный вариант – выглядит это привычнее, чем Express или Fastify. Я бы подумал, что он неплох для новичков из-за того, что пропагандирует архитектурный стиль DDD и TypeScript, но не уверен, что любовь к лишним абстракциям не уничтожит энтузиазм.
Откуда эта сложность?
Сервер, написанный на NEST местами подозрительно напоминает код на Java Spring. А Spring – фактически эталон для написания серверов для крупного бизнеса. Создаётся впечатление, что он взят за образец, хотя авторы чаще упоминают Angular. Но в Java все эти Inversion of Control, Dependency Injection, Singletone, etc. вызваны архитектурой языка. Методология, предложенная Spring является упрощением по отношению к спецификации Enterprise JavaBeans – специфическим правилам для написания классов с бизнес-логикой.
В Java все файлы содержат классы, там не может быть линейного кода, который сразу исполняется при импорте. Если класс что-то должен сделать, то его надо куда-то импортировать и в родительском коде сделать от него экземпляр (за редким статистическим исключением). Плюс — это язык с номинальной типизацией – если какой-то метод принимает объект какого-то класса, то этот объект должен быть явно им порождён – т.е подойдёт не любой User, а из конкретного пакета и конкретного класса (или интерфейса) – а из другого пакета не подойдёт, даже если там тоже есть поле name, а вам кроме этого поля ничего не нужно. Эти особенности порождают всякие специфические концепции и паттерны, типа SOLID, где часть про интерфейсы слабо применима к TypeScript или Golang. Или вот паттерн Singletone – вещь абсолютно бессмысленная в JS/TS. К тому же если вы пишете REST сервер на джаве, то скорее всего вы пишете не сервер как в ноде, а используете готовый (Tomcat или Glassfish) и пишете всякие обработчики для этих серверов, которые потом надо как-то инжектить в готовый сервер.
А что, так можно было?
TypeScript/JavaScript – мультипарадигменный язык – в нём можно в «классы» как в джаве, можно в «модули» как в голанге, можно просто линейный код, как в процедурных языках. Импортированный файл (модуль) – всегда singletone и если вы в модуле вместо класса экспортировали его экземпляр, то это тоже автоматически singletone. В TypeScript типизация структурная – т.е. любой User подойдёт если в нём есть строковое поле name – не нужно создавать набор интерфейсов и имплементировать их (но можно, если очень хочется). А в Node есть объект http который позволяет написать сервер в три строки (не потому, что он непомерно крут, а потому, что это была цель создания Node.js). В результате вся эта стилистика, напоминающая Spring мало уместна в JS и не способна ничего дать, кроме ложного ощущения причастности к большому “энтерпрайзу”.
Как мешает излишняя сложность?
Теоретически — ну и что такого? Натаскали концепций из соседнего языка – пусть
Сложность с отладкой. В основном отладка работает, но иногда что-то сбивается в .map-файлах и ошибки начинают ссылаться на скомпилированные JS файлы. А они завёрнуты по несколько раз декораторами и слабо напоминают оригиналы. Из-за этого бывает сложнее найти изначальное место бага.
“Бунтующие” зависимости, вызывающие замечание типа “Fix the upstream dependency conflict”. Это когда одна из зависимостей обновилась и стала несовместима с другой зависимостью. Очередной такой случай привёл нас к отказу от NEST после трёх лет использования. В одной из вторичных зависимостей поменяли определение типов (в TS) — т. е. JS код никак не изменился, поэтому авторы не посчитали нужным изменять номер версии. В результате код перестал собираться - ведь вызов метода перестал соответствовать новой сигнатуре. Но мы в своём коде этот метод и не вызываем – он вызывается где-то внутри NEST-кода. Единственное решение – даунгрейдить NEST, чтобы он использовал старую версию подзависимости.
Чем предпочли заменить
NEST под капотом использует express или fastify. Мы снесли надстройку и оставили fastify. Он оказался куда как менее чувствителен к смене версий Node и зависимостей. А готовая сборка меньше весит и быстрее деплоется.
Сохранили TypeScript, т. к. это реально работающий инструмент, который упрощает написание сложного кода и взаимодействие с запутанными вложенными объектами.
Придерживаемся архитектурного стиля DDD, скорее даже в JAVA-варианте с Data Transfer Objects, репозиториями, сервисами, REST-контроллерами. Поскольку это держит код структурно разделённым и использует распространённую стандартную терминологию. Когда JAVA-программисту приходится залезать в наш репозиторий у него не возникает вопросов – несмотря на другой язык, структура понятна и прозрачна.
Облегчённая схема настолько понравилась, что не только новые проекты стали писать без NEST, но и переделали пару старых – т. к. никому не захотелось месить старую кашу из dependencies на Ноде которая отстаёт от текущей на 6 версий. Переход оказался не сильно трудоёмким, поскольку в обоих случаях мы организовывали код в соответствии с одними и теми же принципами DDD.
Автор: mkant