company_banner

Архитектура современных корпоративных Node.js-приложений

    Ох, не зря в названии намёк на нетленку Фаулера. И когда фронтенд-приложения успели стать настолько сложными, что мы начали рассуждать о высоких материях? Node.js… фронтенд… погодите, но Нода же на сервере, это бэкенд, а там ребята и так всё знают!



    Давайте по порядку. И сразу небольшой дисклеймер: статья написана по мотивам моего выступления на Я.Субботнике Pro для фронтенд-разработчиков. Если вы занимаетесь бэкендом, то, возможно, ничего нового для себя не откроете. Здесь я попробую обобщить свой опыт фронтендера в крупном энтерпрайзе, объяснить, почему и как мы используем Node.js.

    Определимся, что мы в рамках этой статьи будем считать фронтендом. Оставим в стороне споры о задачах и сконцентрируемся на сути.

    Фронтенд — часть приложения, отвечающая за отображение. Он может быть разным: браузерным, десктопным, мобильным. Но всегда остаётся важная черта — фронтенду нужны данные. Без бэкенда, который эти данные предоставит, он бесполезен. Здесь и проходит достаточно чёткая граница. Бэкенд умеет ходить в базы данных, применять к полученным данным бизнес-правила и отдавать результат фронтенду, который данные примет, шаблонизирует и выдаст красоту пользователю.

    Можно сказать, что концептуально бэкенд нужен фронтенду для получения и сохранения данных. Пример: типичный современный сайт с клиент-серверной архитектурой. Клиент в браузере (назвать его тонким язык уже не повернётся) стучится на сервер, где крутится бэкенд. И, конечно, везде есть исключения. Есть сложные браузерные приложения, которым не нужен сервер (этот случай мы не будем рассматривать), и есть необходимость исполнения фронтенда на сервере — то, что называется Server Side Rendering или SSR. Давайте с него и начнём, потому что это самый простой и понятный случай.

    SSR


    Идеальный мир для бэкенда выглядит примерно так: на вход приложения поступают HTTP-запросы с данными, на выходе мы имеем ответ с новыми данными в удобном формате. Например, JSON. HTTP API легко тестировать, понятно, как разрабатывать. Однако жизнь вносит коррективы: иногда одного API недостаточно.

    Сервер должен отвечать готовым HTML, чтобы скормить его краулеру поисковой системы, отдать превью с метатегами для вставки в социальную сеть или, что ещё важнее, ускорить ответ на слабых устройствах. Совсем как в древние времена, когда мы разрабатывали Web 2.0 на PHP.

    Всё знакомо и давно описано, но клиент изменился — в него пришли императивные клиентские шаблонизаторы. В современном вебе бал правит JSX, о плюсах и минусах которого можно рассуждать долго, вот только одно отрицать нельзя — в серверном рендеринге не обойтись без JavaScript-кода.

    Получается, когда нужна реализация SSR силами бэкенд-разработки:

    1. Смешиваются зоны ответственности. Бэкенд-программисты начинают отвечать за отображение.
    2. Смешиваются языки. Бэкенд-программисты начинают работать с JavaScript.

    Выход — отделить SSR от бэкенда. В простейшем случае мы берём JavaScript runtime, ставим на него самописное решение или фреймворк (Next, Nuxt и т.д.), работающий с нужным нам JavaScript-шаблонизатором, и пропускаем через него трафик. Привычная схема в современном мире.

    Так мы уже немножко пустили фронтенд-разработчиков на сервер. Давайте перейдём к более важной проблеме.

    Получение данных


    Популярное решение — создание универсальных API. Эту роль чаще всего берёт на себя API Gateway, умеющий опрашивать множество микросервисов. Однако здесь тоже возникают проблемы.

    Во-первых, проблема команд и зон ответственности. Современное большое приложение разрабатывают множество команд. Каждая команда сконцентрирована на своём бизнес-домене, имеет свой микросервис (или даже несколько) на бэкенде и свои отображения на клиенте. Не будем вдаваться в проблему микрофронтендов и модульности, это отдельная сложная тема. Предположим, что клиентские отображения полностью разделены и являются мини-SPA (Single Page Application) в рамках одного большого сайта.

    В каждой команде есть фронтенд и бэкенд-разработчики. Каждый работает над своим приложением. API Gateway может стать камнем преткновения. Кто за него отвечает? Кто будет добавлять новые эндпоинты? Отдельная суперкоманда API, которая будет вечно занята, решая проблемы всех остальных на проекте? Какова будет цена ошибки? Падение этого шлюза положит всю систему целиком.

    Во-вторых, проблема избыточных/недостаточных данных. Давайте посмотрим, что происходит, когда два разных фронтенда используют один универсальный API.



    Эти два фронтенда сильно отличаются. Им нужны разные наборы данных, у них разный релизный цикл. Вариативность версий мобильного фронтенда максимальна, поэтому мы вынуждены проектировать API с максимальной обратной совместимостью. Вариативность веб-клиента низка, фактически мы должны поддерживать только одну предыдущую версию, чтобы снизить количество ошибок в момент релиза. Но даже если «универсальный» API обслуживает только веб-клиентов, мы всё равно столкнёмся с проблемой избыточных или недостаточных данных.



    Каждому отображению требуется отдельный набор данных, вытащить который желательно одним оптимальным запросом.

    В таком случае нам не подойдёт универсальный API, придётся разделить интерфейсы. Значит, потребуется свой API Gateway под каждый фронтенд. Слово «каждый» здесь обозначает уникальное отображение, работающее со своим набором данных.



    Мы можем поручить создание такого API бэкенд-разработчику, которому придётся работать с фронтендером и реализовывать его хотелки, либо, что гораздо интереснее и во многом эффективнее, отдать реализацию API команде фронтенда. Это снимет головную боль из-за реализации SSR: уже не нужно ставить прослойку, которая стучится в API, всё будет интегрировано в одно серверное приложение. К тому же, контролируя SSR, мы можем положить все необходимые первичные данные на страницу в момент рендера, не делая дополнительных запросов на сервер.

    Такая архитектура называется Backend For Frontend или BFF. Идея проста: на сервере появляется новое приложение, которое слушает запросы клиента, опрашивает бэкенды и возвращает оптимальный ответ. И, конечно же, это приложение контролирует фронтенд-разработчик.



    Больше одного сервера в бэкенде? Не проблема!



    Независимо от того, какой протокол общения предпочитает бэкенд-разработка, мы можем использовать любой удобный способ общения с веб-клиентом. REST, RPC, GraphQL — выбираем сами.

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

    К сожалению, эффективная работа с GraphQL невозможна без тесного сотрудничества с бэкендерами, которые берут на себя разработку эффективных запросов к базе данных. Выбрав такое решение, мы снова потеряем контроль над данными и вернёмся к тому, с чего начинали.


    Можно, конечно, но неинтересно (для фронтендера)

    Что же, давайте реализовывать BFF. Конечно, на Node.js. Почему? Нам нужен единый язык на клиенте и сервере для переиспользования опыта фронтенд-разработчиков и JavaScript для работы с шаблонами. А как насчёт других сред исполнения?



    GraalVM и прочие экзотические решения проигрывают V8 в производительности и слишком специфичны. Deno пока остаётся экспериментом и не используется в продакшене.

    И ещё один момент. Node.js — удивительно хорошее решение для реализации API Gateway. Архитектура Ноды позволяет использовать однопоточный интерпретатор JavaScript, объединённый с libuv, библиотекой асинхронного I/O, которая, в свою очередь, использует тред-пул.



    Долгие вычисления на стороне JavaScript бьют по производительности системы. Обойти это можно: запускать их в отдельных воркерах или уносить на уровень нативных бинарных модулей.

    Но в базовом случае Node.js не подходит для операций, нагружающих CPU, и в то же время отлично работает с асинхронным вводом/выводом, обеспечивая высокую производительность. То есть мы получаем систему, которая сможет всегда быстро отвечать пользователю, независимо от того, насколько нагружен вычислениями бэкенд. Обработать эту ситуацию можно, мгновенно уведомляя пользователя о необходимости подождать окончания операции.

    Где хранить бизнес-логику


    В нашей системе теперь три большие части: бэкенд, фронтенд и BFF между ними. Возникает резонный (для архитектора) вопрос: где же держать бизнес-логику?



    Конечно, архитектор не хочет размазывать бизнес-правила по всем слоям системы, источник правды должен быть один. И этот источник — бэкенд. Где ещё хранить высокоуровневые политики, как не в наиболее близкой к данным части системы?



    Но в реальности это не всегда работает. Например, приходит бизнес-задача, которую можно эффективно и быстро реализовать на уровне BFF. Идеальный дизайн системы это классно, но время — деньги. Иногда приходится жертвовать чистотой архитектуры, а слои начинают протекать.



    Можем ли мы получить идеальную архитектуру, отказавшись от BFF в пользу «полноценного» бэкенда на Node.js? Кажется, в этом случае не будет протечек.


    Не факт. Найдутся бизнес-правила, перенос которых на сервер ударит по отзывчивости интерфейса. Можно до последнего сопротивляться этому, но избежать полностью, скорее всего, не получится. Логика уровня приложения тоже проникнет на клиент: в современных SPA она размазана между клиентом и сервером даже в случае, когда есть BFF.


    Как бы мы ни старались, бизнес-логика проникнет в API Gateway на Node.js. Зафиксируем этот вывод и перейдём к самому вкусному — имплементации!

    Big Ball of Mud


    Самое популярное решение для Node.js-приложений в последние годы — Express. Проверенное, но уж больно низкоуровневое и не предлагающее хороших архитектурных подходов. Основной паттерн — middleware. Типичное приложение на Express напоминает большой комок грязи (это не обзывательство, а антипаттерн).

    const express = require('express');
    const app = express();
    const {createReadStream} = require('fs');
    const path = require('path');
    const Joi = require('joi');
    app.use(express.json());
    const schema = {id: Joi.number().required() };
    
    app.get('/example/:id', (req, res) => {
        const result = Joi.validate(req.params, schema);
        if (result.error) {
            res.status(400).send(result.error.toString()).end();
            return;
        }
        const stream = createReadStream( path.join('..', path.sep, `example${req.params.id}.js`));
        stream
            .on('open', () => {stream.pipe(res)})
            .on('error', (error) => {res.end(error.toString())})
    });

    Все слои перемешаны, в одном файле находится контроллер, где есть всё: инфраструктурная логика, валидация, бизнес-логика. Работать с этим больно, поддерживать такой код не хочется. А можем ли мы писать на Node.js код энтерпрайз-уровня?



    Для этого требуется кодовая база, которую легко поддерживать и развивать. Иначе говоря, нужна архитектура.

    Архитектура Node.js-приложения (наконец-то)


    «Цель архитектуры программного обеспечения — уменьшить человеческие трудозатраты на создание и сопровождение системы».

    Роберт «Дядя Боб» Мартин

    Архитектура состоит из двух важных вещей: слоёв и связей между ними. Мы должны разбить наше приложение на слои, не допустить протечек из одного в другой, правильно организовать иерархию слоёв и связи между ними.

    Слои


    Как разбить приложение на слои? Есть классический трёхуровневый подход: данные, логика, представление.



    Сейчас такой подход считается устаревшим. Проблема в том, что основой являются данные, а значит, приложение проектируется в зависимости от того, как данные представлены в БД, а не от того, в каких бизнес-процессах они участвуют.

    Более современный подход предполагает, что в приложении выделен доменный слой, который работает с бизнес-логикой и является представлением реальных бизнес-процессов в коде. Однако если мы обратимся к классическому труду Эрика Эванса Domain-Driven Design, то обнаружим там такую схему слоёв приложения:



    Что здесь не так? Казалось бы, основой приложения, спроектированного по DDD, должен быть домен — высокоуровневые политики, самая важная и ценная логика. Но под этим слоем лежит вся инфраструктура: слой доступа к данным (DAL), логирование, мониторинг, и т. д. То есть политики гораздо более низкого уровня и меньшей важности.

    Инфраструктура оказывается в центре приложения, и банальная замена логгера может привести к перетряхиванию всей бизнес-логики.



    Если мы снова обратимся к Роберту Мартину, то обнаружим, что в книге Clean Architecture он постулирует иную иерархию слоёв в приложении, с доменом в центре.



    Соответственно, все четыре слоя должны располагаться иначе:



    Мы выделили слои и определили их иерархию. Теперь перейдём к связям.

    Связи


    Вернёмся к примеру с вызовом логики пользователя. Как избавиться от прямой зависимости от инфраструктуры, чтобы обеспечить правильную иерархию слоёв? Есть простой и давно известный способ разворота зависимостей — интерфейсы.



    Теперь высокоуровневый UserEntity не зависит от низкоуровневого Logger. Наоборот, он диктует контракт, который нужно реализовать, чтобы включить Logger в систему. Замена логгера в данном случае сводится к подключению новой реализации, соблюдающей тот же контракт. Важный вопрос — как её подключать?

    import {Logger} from ‘../core/logger’;
    class UserEntity { 
    	private _logger: Logger;
    	constructor() {
    		this._logger = new Logger();
    	}
    	...
    }
    ...
    const UserEntity = new UserEntity();

    Слои связаны жёстко. Есть завязка и на файловую структуру, и на реализацию. Нам нужна инверсия зависимости (Dependency Inversion), делать которую мы будем с помощью внедрения зависимости (Dependency Injection).

    export class UserEntity {
    	constructor(private _logger: ILogger) { }
    	...
    }
    ...
    const logger = new Logger();
    const UserEntity = new UserEntity(logger);

    Теперь «доменный» UserEntity больше ничего не знает о реализации логгера. Он предоставляет контракт и ожидает, что реализация будет соответствовать этому контракту.

    Конечно, ручная генерация экземпляров инфраструктурных сущностей дело не самое приятное. Нужен корневой файл, в котором мы будем всё подготавливать, придётся как-то протащить созданный экземпляр логгера через всё приложение (выгодно иметь один, а не создавать множество). Утомительно. И здесь вступают в игру IoC-контейнеры, которые могут взять на себя эту боллерплейтную работу.

    Как может выглядеть использование контейнера? Например, так:

    export class UserEntity {
    	constructor(@Inject(LOGGER) private readonly _logger: ILogger){ }
    }

    Что здесь происходит? Мы воспользовались магией декораторов и написали инструкцию: «При создании экземпляра UserEntity внедри в его приватное поле _logger экземпляр той сущности, что лежит в IoC-контейнере под токеном LOGGER. Ожидается, что она соответствует интерфейсу ILogger». А дальше IoC-контейнер сделает всё сам.

    Мы выделили слои, определились с тем, как будем их развязывать. Пора выбрать фреймворк.

    Фреймворки и архитектура


    Вопрос простой: уйдя от Express на современный фреймворк, получим ли мы хорошую архитектуру? Давайте посмотрим на Nest:

    • написан на TypeScript,
    • построен поверх Express/Fastify, есть совместимость на уровне middleware,
    • декларирует модульность логики,
    • предоставляет IoC-контейнер.

    Кажется, здесь есть всё, что нам нужно! Ещё и от концепции приложения как цепочки middleware ушли. Но что насчёт хорошей архитектуры?

    Dependency Injection в Nest


    Давайте попробуем сделать всё по инструкции. Так как в Nest термин Entity применяется обычно к ORM, переименуем UserEntity в UserService. Логгер поставляется фреймворком, поэтому вместо него заинжектируем абстрактный FooService.

    import {FooService} from ‘../services/foo.service’;
    @Injectable()
    export class UserService {
    	constructor(
                private readonly _fooService: FooService
       ){ }
    }

    И… кажется, мы сделали шаг назад! Инъекция есть, а инверсии нет, зависимость
    направлена на реализацию, а не на абстракцию.

    Давайте попробуем исправить. Вариант номер один:

    @Injectable()
    export class UserService {
    	constructor(
                private _fooService: AbstractFooService
       ){ } }

    Где-то рядом описываем и экспортируем этот абстрактный сервис:

    export {AbstractFooService};

    FooService теперь использует AbstractFooService. В таком виде мы регистрируем его вручную в IoC.

    { provide: AbstractFooService, useClass: FooService }

    Второй вариант. Пробуем описанный ранее подход с интерфейсами. Так как в JavaScript не существует интерфейсов, вытащить требуемую сущность из IoC в рантайме, воспользовавшись рефлексией, уже не получится. Мы должны явно указать, что нам нужно. Используем для этого декоратор @​Inject.

    @Injectable()
    export class UserService {
    	constructor(
                @Inject(FOO_SERVICE) private readonly _fooService: IFooService
       ){ } }

    И регистрируем по токену:

    { provide: FOO_SERVICE, useClass: FooService }

    Победили фреймворк! Но какой ценой? Мы отключили довольно много сахара. Это подозрительно и наводит на мысль, что не стоит укладывать всё приложение во фреймворк. Если я вас ещё не убедил, есть и другие проблемы.

    Исключения


    Nest прошит исключениями. Более того, он предлагает использовать выбрасывание исключений для описания логики поведения приложения.



    Всё ли тут в порядке с точки зрения архитектуры? Снова обратимся к корифеям:
    «Если ошибка — это ожидаемое поведение, то вы не должны использовать исключения».
    Мартин Фаулер
    Исключения предполагают исключительную ситуацию. При написании бизнес-логики мы должны избегать выбрасывания исключений. Хотя бы по той причине, что ни JavaScript, ни TypeScript не дают гарантий, что исключение будет обработано. Более того, оно запутывает поток исполнения, мы начинаем программировать в GOTO-стиле, а значит, во время исследования поведения кода читателю придётся прыгать по всей программе.



    Есть простое правило, помогающее понять, законно ли использование исключений:
    «Будет ли код работать, если я удалю все обработчики исключений?» Если ответ «нет», то, возможно, исключения используются в неисключительных обстоятельствах».
    The Pragmatic Programmer
    Можно ли избежать этого в бизнес-логике? Да! Необходимо минимизировать выбрасывание исключений, а для удобного возврата результата сложных операций использовать монаду Either, которая предоставляет контейнер, находящийся в состоянии успеха или ошибки (концепция, очень близкая к Promise).

    const successResult = Result.ok(false);
    const failResult = Result.fail(new ConnectionError())

    К сожалению, внутри предоставляемых Nest сущностей мы часто не можем действовать иначе — приходится выбрасывать исключения. Так устроен фреймворк, и это очень неприятная особенность. И снова возникает вопрос: может быть, не стоит прошивать приложение фреймворком? Может, получится развести фреймворк и бизнес-логику по разным архитектурным слоям?

    Давайте проверим.

    Сущности Nest и архитектурные слои


    Суровая правда жизни: всё, что мы пишем с помощью Nest, можно уложить в один слой. Это Application Layer.



    Мы не хотим пускать фреймворк глубже в бизнес-логику, чтобы он не прорастал в неё своими исключениями, декораторами и IoC-контейнером. Авторы фреймворка будут раскатывать, как здорово писать бизнес-логику, используя его сахар, но их задача — навсегда привязать вас к себе. Помните, что фреймворк — лишь способ удобно организовать логику уровня приложения, подключить к нему инфраструктуру и UI.


    «Фреймворк — это деталь».
    Роберт «Дядя Боб» Мартин



    Приложение лучше проектировать как конструктор, в котором легко заменить составные части. Один из примеров такой реализации — гексагональная архитектура (архитектура портов и адаптеров). Идея интересна: доменное ядро со всей бизнес-логикой предоставляет порты для общения с внешним миром. Всё, что нужно, подключается снаружи через адаптеры.



    Реально ли реализовать такую архитектуру на Node.js, используя Nest как основу? Вполне. Я сделал урок с примером, если интересно — ознакомиться можно по ссылке.

    Подведём итоги


    • Node.js — это хорошо для BFF. С ней можно жить.
    • Готовых решений нет.
    • Фреймворки не важны.
    • Если ваша архитектура становится слишком сложной, если вы упираетесь в типизацию — возможно, выбран не тот инструмент.

    Рекомендую эти книги:

    Яндекс
    Как мы делаем Яндекс

    Комментарии 54

      +5

      Возможно, кому-то понравятся высказанные в статье подходы, но в тех приложениях, которые я писал, это либо неактуально, либо избыточно.


      1. То, что можно делать SSR на ноде, создавая сервер-прослойку (BFF) — тема избитая и в корпоративных приложениях используется уже несколько лет.
      2. Проблема необходимости разного набора данных разным приложениям решается версионированием АПИ, тут ничем BFF не поможет
      3. API Gateway как маппер, в какой микросервис пойти в зависимости от урла запроса (https://api.domain.com/ms1/users | https://api.domain.com/ms2/auth) и по какому протоколу, действительно можно вынести в BFF, но фронтендерам придется следить за изменением инфраструктуры бэковых сервисов и синхронизироваться. Удобнее все же отдать реализацию этого маппера бэкендерам, в BFF же просто проксируя запросы в единую точку.
      4. То, что бизнес-логика в виде нормализации данных, стейт-машин, синхронизации между различными источниками (офлайн режим, сторонние сервисы), проверка прав есть и на фронтенде — это логично. Делать запрос на бэк "юзер нажал кнопку Восстановить пароль, есть ли у него права для отображения этой страницы?" или "можно ли ему показать кнопку редактирования?" действительно нет смысла, если можно проверить эти права во фронтовом стейте с пермишенами, полученными заранее. Хотя это и выглядит "размазыванием ответственности", в основном этот код обслуживает нужды интерфейса.
      5. Пример кода на Express, в котором "все слои перемешаны" — дело рук не этого простого, гибкого и удобного инструмента, а разработчика. Конечно, можно написать любую дичь и сказать "поддерживать такой код не хочется", но вот нападки на Express этим не обоснованы. Валидации, обработка ошибок, логирование, метод отправки на фронт (стримами в данном случае), миддлвары обработки запроса, схемы — все это выносится в удобные слои, которые хоть и склеиваются внутри миддлвар в итоге, но позволяют абстрагироваться.
      6. "протащить созданный экземпляр логгера через всё приложение" можно и через global (ничего страшного в таких микроприложениях не будет) или более удобным декоратором @useLogger class UserEntity extends EntityClass {}, для чего дополнительные невидимые контейнеры — не понятно. Это же BFF, простой сервер-рендерер-прокси, а не сложная система с кучей сервисов.
      7. Миддлвары — отличный паттерн, явно определяющий порядок обработки запроса и позволяющий удобно логировать этапы и в любой момент прервать этот поток, выбросив исключение, либо отправив response. При этом структурировать обработку можно как угодно — через мапперы, классы, функциональные контроллеры, схемы обработки, описанные в json, — все зависит от видения разработчика и применяемых подходов. Nest переусложнен и жестковато структурирован для BFF, в котором основной функционал — по схеме роутинга отдать рендер фронтендового фреймворка + проксировать запросы на бэк с соответствующими валидациями и нормализацией + логирование + работа с сессиями. Express идеален для подобных задач благодаря своей простоте и развитой экосистеме.

      Ну и как-то я не уловил момент перехода с BFF с простым примером на всяческие UserEntity, домены, сервисы, пайпы, гарды… Это, видимо, когда фронтендеру после создания сервера-прокси пришла мысль "а почему бы весь бэк не переделать на ноду? Я могу!") Ну максимум там будут сервисы хранения сессии пользователей и кэширования. Или BFF был просто вступлением, а суть — в рекламе ролика с гексагональным паттерном для высчитывания остатка денег на счету пользователя? Критикую не ролик, а несоответствие теме статьи и в целом не совсем логичное повествование.

        +4
        Привет, спасибо за замечания!

        Проблема необходимости разного набора данных разным приложениям решается версионированием АПИ, тут ничем BFF не поможет


        В данном случае речь не о поддержке нескольких версий, а об оптимальном наборе данных для каждой страницы/приложения и уменьшении количества запросов, необходимых для получения этих данных. Для примера, на одной странице нам можем понадобится только имя пользователя, на другой нам нужно получить имя, ссылку на аватар и счётчик комментариев. Используя один универсальный эндпоинт мы будем либо вытягивать недостаточно данных и делать дополнительные запросы, либо тащить избыточный набор и нагружать канал.

        API Gateway как маппер,, в какой микросервис пойти в зависимости от урла запроса

        В нашем случае всегда есть оркестрация, недостаточно иметь простой маппер. Например, авторизация пользователя — это отдельный микросервис. Да, мы стараемся минимально зашивать бизнес-логику, но коммуникаций между микросервисами бэкенда у нас в избытке.

        Конечно, можно написать любую дичь и сказать «поддерживать такой код не хочется», но вот нападки на Express этим не обоснованы

        Пока мой опыт показывает, что качество кода с переходом на Nest в командах выросло. Абсолютно согласен, что можно писать хорошо на Express, но, к сожалению, чем более низкоуровневое решение, тем больше свободы у разработчика.
        Однако я к этому и хотел подвести, что Nest не даст архитектуры, он всего лишь улучшит отдельные моменты. Например, нам удалось построить очень удобную систему распределённого рендеринга (часть страницы рендерится отдельно в отдельном микросервисе), удобно подключаемую через декораторы к контроллерам, что сильно снизило когнитивную нагрузку на разработчика. Код контроллеров остался максимально чистым.

        Это же BFF, простой сервер-рендерер-прокси, а не сложная система с кучей сервисов.

        Как только бизнес-логика начинает проникать в BFF — исчезает «простой рендер прокси», к сожалению. А в больших проектах она будет проникать.

        Миддлвары — отличный паттерн

        Но работающий в Express на неконтролируемом мутировании нетипиризованных Request и Response. От этого бывает очень больно.

        Nest переусложнен и жестковато структурирован для BFF

        Именно этой жесткости нам и не хватало в Express.

        Или BFF был просто вступлением, а суть — в рекламе ролика с гексагональным паттерном для высчитывания остатка денег на счету пользователя?

        Ролик — просто ответ на возможный вопрос «а как же писать бизнес-логику, не завязанную на фреймворк»? Просто, чтобы не быть голословным.
          +1

          Хорошие ответы, только не хватает этих оговорок в самом тексте статьи) Нужен контекст, что примененные подходы работают именно в вашем большом проекте, где:


          • в BFF и API Gateway проникла бизнес-логика и запутанные взаимосвязи (да, в статье об этом говорится, но как о возможности, а не об опорных исходных данных)
          • каждый байт на счету при передаче данных с бэка по апи (очень редкий кейс, характерный только для хайлоада, так как в остальных корп продуктах заботятся о перфомансе выполнения операций, стабильности и унифицированности, но не о наличии некоторых дополнительных данных)
          • архитектурные практики недостаточно контролируются, поэтому востребована система с жестким паттерном

          Нетипизированные req & res в Express действительно проблема при интенсивном использовании, но никто не заставляет использовать их как хранилища. Так, сессии и кэш лучше держать в Redis, как и дополнительные данные типа traceId запросов, а работа с базой отлично типизируется и стандартизируется, что исключит "неконтролируемые мутации". В Nest же, как вижу, предлагается использовать неявный слой IoC, а про дополнительные данные, привязанные к сессии пользователя, не говорится, так что сравнить не с чем. В целом, не думаю, что это значительный недостаток.


          В создании системы распределенного рендеринга участвовал, по сути она состоит из "определить микрофронтенды, которые ответственны за рендеринг страницы" + "ответы от их bff склеить в единый html-документ", эти задачи можно решить массой способов. Наверняка решение "подключить декоратором к контроллеру" продиктовано архитектурой Nest, то есть и в этом случае задействована специфика вашего проекта, в котором жесткие подходы выгоднее свободного архитектурирования. Для максимальной прозрачности удобнее было бы иметь сервис-детектор, который достаточно просто покрывается тестами, а сбор данных произвести через const requiredBffs = detector(currentRoute); const htmlParts = Promise.all(requiredBffs.map(fetch)) через слой кэширования, а затем провести склейку по маркерам. В идеале система должна работать автоматически, а информация о частях, из которых состоит конкретная страница, содержаться в конфиге роутов, что не потребует от разработчиков вообще задумываться об этом механизме и проставлять декораторы. То есть я к тому, что, возможно, жесткие паттерны в данном случае ухудшили экспириенс, а не улучшили)


          И "как же писать бизнес-логику" в BFF — тоже контекстуальная тема, которая не должна затрагивать большинство корп проектов. Когда фронтовый бэк из рендерера для seo и ускорения первой отрисовки превращается в толстый бэк с кучей наворотов, пора бы задуматься, туда ли все повернуло, и не стоит ли остальную логику вынести на "хардкорный" бэк. Так что логичнее было бы вынести это в отдельную статью, сравнив несколько паттернов организации сущностей и взаимодействия между ними, ибо в итоге раздел "архитектурные слои" получился очень немногословным и смятым

            +1
            В Nest же, как вижу, предлагается использовать неявный слой IoC, а про дополнительные данные, привязанные к сессии пользователя, не говорится, так что сравнить не с чем.


            Да, в Nest и правда очень сильно не хватает возможности привязать данные к сессии пользователя. Это решается созданием сервиса в скоупе реквеста (IoC даёт это из коробки), но это довольно дурнопахнущее решение. Другой вариант — использовать CLS (например cls-hooked, либо недавно приземлённые в ноду AsyncLocalStorage)

            Так что логичнее было бы вынести это в отдельную статью, сравнив несколько паттернов организации сущностей и взаимодействия между ними, ибо в итоге раздел «архитектурные слои» получился очень немногословным и смятым


            Ваша правда. Для того и пишем, чтобы учиться доносить мысли лучше. Теперь сам вижу, что скомкано вышло :)
              0
              Вообще, архитектурное решение хорошее, но к нему слишком длинная подводка и описание не очень проблемных проблем. Модель в центре — вот основа, а не ssr и «проблема избыточных/недостаточных данных». Хорошие решенич были известны и до веба, просто веб не перенял опыт Дэлфи, Сибилдера, Сишарпа, Джавы… веб начал все сам изобретать, не читая книг. Вот и накрутили, а теперь только начали немного заглядывать в литературу и повторять то, что уже было и хорошо себя зврекомендовало.
          0

          Мидлвары — сломанная версия паттерна "цепочка ответственности" развращающая не знакомых с SOLID, GRASP и GoF при помощи шаренного состояния и сайдэффектов.

          0

          Исключения не так страшны (в конце концов, если не упираться в идеологическую чистоту, это дело вкуса), как сложности с их отловом в Js/ts по конкретному типу. У меня костыль с этаким функциональным аналогом try-catch (+async) с поиском ближайшего прототипа и типизацией на дженериках.


          Но, конечно, из обработчика команды выкидывать исключение совсем не айс, тут или Either или result object по типу dto.


          Вот что действительно печалит — так это отсутствие нормальной ORM, позволяющей работать с rich models и DDD aggregate roots. Typeorm вообще написан с анемичными моделями как единственным подходом в голове разработчика, mikroorm — более верное направление, но гадит в прототип сущности вместо использования прокси и не умеет ембеддаблы. А на самом деле нужен в основе прежде всего хороший маппер, по типу автомапперов в c#. Хоть сам пиши.

            0
            Исключения не так страшны (в конце концов, если не упираться в идеологическую чистоту, это дело вкуса), как сложности с их отловом в Js/ts по конкретному типу


            Да, если бы мы могли как в Java описывать все возвращаемые исключения по типам — жизнь была бы намного лучше.

            Вот что действительно печалит — так это отсутствие нормальной ORM, позволяющей работать с rich models и DDD aggregate roots.

            К счастью, BFF перекладывает всю ответственность по работе с БД на плечи бэкенда. Боль очень понимаю, но сам не испытываю.
              +2

              Бэкенд-то тоже на чем-то писать надо. :)


              BFF у меня как бы есть (спасибо, теперь я знаю это слово, называл это просто SSR backend), но настолько микроскопический, что вообще все очевидно, NestJS прекрасно подходит, если думать не инструкцией, а головой (завязывать все слои на фреймворк — очевидная глупость, а для http application слоя там все ок).

                +1
                Бэкенд-то тоже на чем-то писать надо. :)

                Имеется в виду, что "надо, но не вам, а команде бекендеров" :D

                  0

                  Тяжела доля фуллстека :)

            –1
            GraalVM и прочие экзотические решения проигрывают V8 в производительности и слишком специфичны.


            Привет! А можно немного по подробнее, в чем GraalVM проигрывает, и на каких тестах?
              +2
              Вот в этом докладе Олег Шелаев говорит, что они на 15-20% медленнее, чем V8. В районе 27-й минуты https://youtu.be/sKS4A9I8xb8
                0
                Спасибо!
                Интересно, что там за год ребята сделали, и какой у них прогресс. Завтра как раз можно будет это спросить :)

                www.youtube.com/watch?v=mYg3f-117Zc
                  0
                  О, спасибо!
              0

              По поводу исключений вопрос: промисовые режекты — в некотором смысле аналоги исключений. Является ли "искусственный" режект плохой практикой?

                +1
                Нет, это не аналог исключений. Разница в том, что исключение выпрыгивает из потока. Наша функция имеет контракт, говорящий о том, что она возвращает/не возвращает набор данных определённого типа. Исключение нарушает этот контракт, всплывая по стеку до первого перехватчика. Указать в контракте в JS/TS факт наличия исключений и их тип мы не можем. Таким образом, потребитель нашего кода не знает не заглядывая в исходники и документацию о том, что может быть порождено исключение и факт его обработки нельзя проверить статически.
                Промис не нарушает контракт. Мы говорим, что наша функция возвращает промис и она его возвращает. Конечно, мы всё ещё сталкиваемся с проблемой статического анализа обработки хорошего и плохого пути (потребитель может не обработать catch), но отловить это намного проще. Но async/await ломает всю эту красоту и мы снова вынуждены писать try/catch. Тут уже вступает в игру result-контейнер.
                  0

                  Одна из базовых характеристик отказоустойчивых систем — явно определенные потоки ошибок, которые попадают в соответствующие перехватчики. То есть, к примеру, при работе с апи бэка можно использовать такой флоу:


                  function authUser({ store, api, actions }, { login, password }) {
                    return Promise.resolve()
                      .then(() => api.postUserData({ login, password }))
                      .then(({ userInfo }) => (store.user = userInfo))
                      .catch(error => {
                        if (error.name === "SOLVED") return;
                  
                        if (error.name === "INVALID_LOGIN") {
                          return actions.pushFormValidators({ 
                            password: { value => value === login, message: 'Пользователя не существует' } 
                          })
                        }
                  
                        return actions.showNotification('Неизвестная ошибка')
                      })
                  }

                  При этом обработка HTTP-ошибок будет производиться в api-слое, то есть имеются следующие сценарии:


                  • при 404/403 метод api.postUserData сам выдаст нотификацию и вернет Promise.reject(Err.SOLVED), таким образом обработчик выше не будет предпринимать действий.
                  • при 402 (ошибка валидации или формата данных, присланная бэком) отдаст Promise.reject(Err[response.errorName]), обработчик подсветит эту ошибку в форме и не даст пользователю второй раз ввести некорректные данные
                  • при неизвестной ошибке в самом экшене authUser (например, опечатка stAre.user = userInfo) либо при багах кода в процессе запроса, ошибка попадет в последнюю строчку кэтчера. Этот же сценарий в случае, если бэк вернет неизвестную константу на фронт вместо "INVALID_LOGIN", то есть при ошибке в контракте общения.

                  Таким образом явно определяются потоки всех возможных ошибок и стратегии их обработки (можно и дополнительные слои выносить, я привел базовый пример), четко ограничивается скоуп их распространения и исключается влияние на остальной функционал приложения. В целом, хотя при throw вместо Promise.reject будет работать так же, для типизации+семантичности все же лучше использовать Promise.reject, тут я с автором согласен, для предсказуемых "ошибок", которые по сути являются перенаправлением в другой блок обработки промиса (catch вместо then). Но учитывать возможность возникновения неожиданных исключений всегда необходимо, то есть, если заменить reject на throw, должно работать так же, иначе может возникнуть ситуация, что при выполнении действия ломается вся страница или большая часть функционала (очень часто встречаю это при работе).

                  –4
                  Коментарии трут, минусуйте тред
                    +2
                    Возможно я нажал не туда, первый раз на Хабре. Можно повторить вопрос?
                    0
                    Я слышал расхожее мнение, что нод приложение по своей производительности не сравнится с корпоративными монстрами вроде java, поэтому бэкенд на ноде часто масштабируют горизонтально.
                    Насколько это оправданные опасения и если речь о действительно больших приложениях и большом количестве запросов, то справится ли BFF слой с нагрузкой?
                      0
                      Я затронул этот момент в посте. Node.js действительно не предназначена для интенсивных CPU-вычислений и приложения на node.js всегда масштабируют либо встроенным механизмом воркер-тредов (порождая несколько процессов внутри материнского) либо внешними средствами. Но в том и соль BFF, что на нём не должно быть нагрузок CPU, только I/O, а тут Node.js прекрасно справляется с огромным количеством входящих запросов, используя модель с event loop и системным демультиплексером.
                      Т.е. с большим количеством запросов года справится, если на эти запросы не будет навешено тяжёлой работы, особенно синхронной.
                        0
                        Тем более, что тяжелую работу всегда можно куда-то вынести, например с помощью очередей
                          0
                          Да, если разделить, то можно жить. Другой вопрос, что система становится очень сложной и нужна соответствующая квалификация, чтобы её поддерживать в живом состоянии.
                            0
                            Для работы с многопоточными системами тоже квалификация нужна =) К тому же из треда обработки http-запроса тяжелые вычисления по-моему чаще выносятся, чтоб http 504 не выхватывать.
                          0
                          , если на эти запросы не будет навешено тяжёлой работы, особенно синхронной.

                          На самом деле сложно представить какую либо длительную синхронную операцию. В теории она существует, но найти реальный кейс вряд ли возможно. Какая синхронная задача может занять более 5-10мс, которая бы повлияла на отзывчивость для других клиентов? Даже если это какие то тяжелые вычисления (которые почему то написаны на JS) всегда можно протащить через ивентлуп, вместо рекурсии например. С асинхронными функциями теперь это весьма просто, только пару строк чтобы следить за тиканьем ивентлупа. Ну а дальше балансировщик сделает свое дело.
                            0
                            Например, разбор графов в graphql запросах достаточно тяжёлый, фактически ребята из core team node.js в открытую заявляют, что производительность node.js-решения недостаточна. Так же могут быть тяжёлые crypto-операции (не зря их выносят в нативные модули). Даже банальное форматирование ответа для логгера лучше выносить в отдельный тред, чтобы не просаживать основной на бесполезных для пользователя операциях.

                            Разбивать тяжёлые операции на чанки хорошее решение, про которое зачастую забывают.
                              0
                              Ну правильно, чанки фактически решают проблему отзывчивости и «блокировки» других запросов, давно ей пользуюсь. Задачу, которую нельзя разбить на чанки я не могу представить, потому как ее нет. Благодаря async/await и немного хелперов вообще не отличается от написания синхронного варианта, уже больше не надо извращаться для такой техники. В итоге мы перегрузку ЦП просто нивелируем добавлениям инстансов в пул балансира, простым горизонтальным масштабированием — простейший вариант. А передавать запросы на воркеры как бы лишние телодвижения.
                              Даже как то удивительно, что ее так мало юзают другие, я в чужих реальных проектах этого не видел, а ведь банальный подход.
                                0
                                Вынос операции в чанки немного разгрузит event loop от голодания, но не даст настоящей параллельности, мы всё равно будем вынуждены делать все операции в том же потоке. Да, можно скейлить горизонтально, но вынос в отдельный тред эффективнее — само по себе дробление это не бесплатная операция, мы забиваем очереди event loop. Разделив разные операции по разным воркерам мы сможем их эффективно масштабировать. Например, для того же форматирования логов хватит одного воркера при 8 обслуживающих пользовательские запросы и т.д.
                                  0
                                  мы всё равно будем вынуждены делать все операции в том же потоке.

                                  Да, но с точки зрения клиентов им есть ли разница в каком потоке на сервере был обработан их запрос? Им интересна только задержка ответа. Если речь о делегировании работы другому потоку (основной получил, передал данные воркеру ожидает ответа, попутно обрабатывая другие запросы в евентлупе) то это ничем не отличается от того, если бы запрос пришел другому инстансу через балансир, только еще одно лишнее звено (балансир->инстанс1->воркер1). Если речь что один запрос можно параллельно обработать- половину задачи одним потоком обработать, пока второй решает вторую половину, то тут уже вылазит проблемы синхронизации потоков и зависимостей данных, т.е задачи могут плохо распараллеливается если следующие вычисления базируются на предыдущих. Но к этому надо добавить оверхед на передачу и получения данных воркерам. А ведь даже тяжелые запросы это всего миллисекунды. Суть ведь равномерно загрузить ядра/цп, чтобы потоки не гуляли, при этом обеспечить допустимую задержку отклика для каждого запроса. Какие то феншуи делать для размазывания по воркерам запросы в миллисекунды как бы бессмысленно для большинства задач в вебе.
                                  само по себе дробление это не бесплатная операция, мы забиваем очереди event loop.

                                  В чем разница? 10 инстансов и 1 инстанса + 9(10) воркеров? Воркеры тоже не бесконечны- не на каждый ведь запрос можно выделить свой воркер, так что будем иметь тоже самое узкое горлышко, только в другом потоке. Дробить задачи так чтобы каждый чанк не исполнялся более пары мс не забьет очередь, чтобы существенно отразилось на производительности. Трансфер данных воркерам тоже не бесплатно, и вероятно дороже.
                                    0
                                    Конечно в каждом конкретном случае надо отдельно оценивать какой вариант даст больше выходы, проводя нагрузочные стрельбы. Отдавать часть вычислений в параллельный воркер и забирать назад действительно дорого, но отдавать инфраструктурные задачи (те же подготовки метрик для Prometheus и API для его обслуживания), отделяя от треда, обслуживающего непосредственно пользовательские запросы выгодно.

                                    Трансфер данных воркерам тоже не бесплатно, и вероятно дороже.

                                    По IPC да, но, к счастью, теперь у нас есть worker threads с более дешёвой коммуникацией через shared memory.
                                      +1
                                      Передача cpu-жрущей операции в воркер существенно дешевле передачи в другой инстанс: передавать можно через SharedArrayBuffer и MessagePort, а при использовании CAS можно даже общаться через расшаренную память, см. мой доклад: «Разделяемая память в многопоточном Node.js. Тимур Шемсединов. JS Fest 2019 Spring»
                                      и лекции:
                                        +1
                                        Несомненно дешевле, чем форк IPC, но не дешевле чем если ничего никуда не передавать и выполнять силами текущего инстанса, а остальные запросы пусть балансир другим пихает :)

                            0

                            Сравнение некорректное. Java это язык а нода это рантайм (конкретная реализация взаимодействия с IO операционной системы). И скорость ноды и этой хваленый event loop и т.д — это все заслуга системного IO операционной системы — например на линуксе это системный вызов epoll_wait(..) — который позволяет одним потоком обслуживать много сокетов. Это значит что аналогичную эффективность NodeJS (скорости работы с io и обслуживание одним потоком многих tcp/http запросов) можно получить на всех популярных языках — не только на java но и например на php — достаточно всего лишь прокинуть системные вызовы epoll_wait/epoll_create/… в интерпретатор или компилятор языка.

                              +1
                              Если это вопрос ко мне, то я не сравниваю производительность асинхронного java кода с node.js. Я говорю о том, что производительность node.js достаточная для работы в режиме BFF-прослойки и мы не деградируем систему несмотря на однопоточную природу JavaScript.

                              Но уточнение резонное, хорошая поддержка асинхронного I/O была сильной стороной node.js только в момент появления на рынке, сейчас все остальные решения так же подтянулись и могут предложить свой вариант async I/O.
                            0
                            Бизнес логика на стороне данных (т.е. в БД в виде stp) уже даже не рассматривается? Вот это поворот.
                              +1
                              Давно идёт война между этими фандомами. Моё личное мнение, что на уровень БД выносить бизнес-логику можно только в тех случаях, когда это даст критически важное ускорение для системы. Это очень жёсткий трейдофф, и компании, пережившие переезды между несколькими БД, хорошо это понимают.
                              Но как фронтендер я не могу тут спорить — опыта не хватит.
                                0
                                Если запросы затрагивают большое количество данных, то обработка данных на стороне данных — это фактически единственный жизнеспособный вариант.
                                  0
                                  Да, если нам критично время ответа. Однако, мне кажется, что такие задачи встречаются не часто, во многих системах работать с данными можно асинхронно. Но это только ощущения, без статистики.
                              0

                              Одобряю! Почти полностью совпадает с моими 3 весенними вебинарами на jsfwdays: https://fwdays.com/en/event/node-js-in-2020
                              Кроме одного: декораторы — зло, nest.js сделал большую работу по уничтожению центра говнокода (express) и но он умрет из-за TypeScript.

                                +1
                                Не хотел затрагивать ещё и тему декораторов. Но да, безусловно, это может стать большой проблемой nest-проектов.
                                Но что делать, все реализации IoC-контейнеров для ts построены на легаси-декораторах.
                                  0
                                  У меня в NodeJsStarterKit сделан DI на для JS на базе vm.createContext и vm.runInContext.
                                  А сейчас я делаю возможность для impress писать не только js методы, но ts и AssemblyScript. При этом, все метаданные про методы описываются декларативно, пример:
                                  ({
                                    access: 'public',
                                    method: async ({ login, password }) => {
                                      const user = await application.auth.getUser(login);
                                      const hash = user ? user.password : undefined;
                                      const valid = await application.security.validatePassword(password, hash);
                                      if (!user || !valid) throw new Error('Incorrect login or password');
                                      console.log(`Logged user: ${login}`);
                                      return { result: 'success', userId: user.id };
                                    }
                                  });
                                  

                                  а код методов может выглядеть очень кратко:
                                  async ({ countryId }) => {
                                    const fields = ['Id', 'Name'];
                                    const where = { countryId };
                                    const data = await application.db.select('City', fields, where);
                                    return { result: 'success', data };
                                  };
                                  

                                  или даже
                                  ({ countryId, minArea, maxArea }) => application.db
                                    .select(['cityId', 'cityName', 'population'])
                                    .from('locality')
                                    .where({
                                      countryId,
                                      lacalityType: 'city',
                                      population: { '>': 1000000 },
                                      area: { between: [minArea, maxArea] },
                                    })
                                    .timeout(5000)
                                    .cache(2000)
                                    .paging(100);
                                  

                                  Уже сейчас есть: подгрузка изменений с hd без перезагрузки процессов, утилизация CPU через потоки, очередь запросов на вход в API, авто балансировка и куча всего. В стартер ките все это поместилось в 25кб без зависимостей (для ознакомительных с внутренностями целей), а в impress все так же, но с оптимизациями и распилено на npm-модули. Можно посмотреть архитектуру в стартер-ките, но платформа будет готова только к концу года, с прозрачным масштабированием стейтфул приложений.
                                    +1
                                    Начал смотреть вебинар и наткнулся там на эти исходники. Очень интересно!
                                      0
                                      Ни каких декораторов, ни каких require, ни каких мидлваров даже под капотом, в прикладном слое вообще нет global и все интерфейсы под фризом, вместо orm схемы, и схемы могут валидировать как объекты предметной области, так и структуры данных и интерфейсы (сигнатуры методов). Но еще очень много делать, хотя у нас грант на это есть и несколько крупных компаний внедряет и помогает, все будет в оупенсорсе.
                                        0
                                        Пример схемы, в ней и методы могут быть (прямо тебе дата-центрическая архитектура с доменом всередине):
                                        ({
                                          Category: { category: 'Category', index: true },
                                          StorageKind: { domain: 'StorageKind', required: true, index: true },
                                          Status: { domain: 'IdStatus', required: true, index: true },
                                          Creation: { domain: 'DateTime', required: true },
                                          Change: { domain: 'DateTime', required: true },
                                          Lock: { domain: 'Logical', required: true, default: false },
                                          Version: { domain: 'Version', required: true, default: 0 },
                                          Checksum: { domain: 'SHA2', required: true },
                                        
                                          CheckCategory: Validate(record => {
                                            if (record.Status === 'Actual' || record.Status === 'Historical') {
                                              return !!record.Category;
                                            } else {
                                              return !record.Category;
                                            }
                                          })
                                        });
                                        

                                        Еще примеры: github.com/metarhia/globalstorage/tree/master/schemas/system
                                    +1
                                    Посмотрел вебинар — действительно, многие вещи пересекаются. Вот что заитересовало (я не раскрывал этот момент в посте) — ты предлагаешь отказаться полностью от кластеризации силами встроенного модуля cluster в node.js и, тем более, (не к ночи будет помянут) pm2, поднимая worker_threads каждый на отдельном порту. Но кто в этом случае будет заниматься балансировкой? В эксплуатацию передаётся приложение с набором портов и просьбой добавить балансер?

                                    И, второй вопрос, как поступать в случае контейнеризации? Положить один «большой» процесс с кучей worker_threads в контейнер и поставить балансировщик? Мы хотели двигаться в несколько иную сторону, выделяя каждый процесс (воркер) в отдельный контейнер и масштабируясь контейнерами. Схожий подход описан в Node.js Design Patterns, даже в самой свежей её редакции.
                                      +1
                                      Модуль cluster это плохой способ масштабирование, если у нас более 6-8 процессов, все подключения проходят через родительский процесс, а потом передаются по IPC в дочерние, на большом кол-ве дочерних можно видеть, 100% загрузку главного процесса и недогрузку дочерних.

                                      Я предлагаю масштабироваться при помощи тредов открывая по 1 порту в каждом, а балансировать в отдельном Auth-сервисе, при входе или первом контакте клиент получает токен и несколько точек подключения host:port, подключается к первой по токену и попадает в свой родной тред. При чем, я преимущественно во всех проектах использую вебсокеты для веба и TLS для мобильных (дальше планирую на http3 переходить). Если связь пропадает, то параллельно пробуем переподключиться к основному и запасным точкам подключения, если все открылись, то берем предпочтительную (основную) если основной сокет не подключается, то переходим на запасные. Таким образом не нужно весь трафик пропускать через один балансировщик, а распределение происходит только при создании сессиий.

                                      Контейнеры: на моих проектах мы делаем 1 контейнер на машину, внутри контейнера масштабирование тредами. Машины у нас железные, но и для виртуалок я бы такую же схему использовал.

                                      Готового решения я сейчас не могу предоставить, но целая группа в которой много контрибьютеров ноды, работает над технологическим стеком Метархия, который будет готов для продакшена к концу года, в нем масштабирование будет решено из коробки. А пока можно смотреть прототип в моем Статрет ките и помогать нам все это быстрее привести в порядок.
                                        +1
                                        Я считаю, что статья очень полезная, в тектовом виде нет аналогичных. Все до сих пор радуются мидлварам, я с 2012 года говорил, что это маразм, и ты чуть ли ни первый, кто это тоже понимает. Ну и по другим вопросам, домен в центре — это классика, я до этого на C#, C Builder, Delphi так писал и просто перенес это в ноду, но тут так ни кто не пишет. Переломить этот стиль сложно.
                                          +2
                                          Я записал 200 часов лекций, но их мало кто смотрит и все равно все пишут лапшу, учась по говнокурсам. Большая проблема мидлваров — что они создают shared state, а люди думают, что в однопоточном языке они защищены от проблем состояния гонки (race conditions), но это не так, о чем у меня есть несколько докладов на jsFest и fwdays. Я не против шаред стейта, но нужно уметь его готовить, нужны блокировки, мьютексы, семафоры, а ни кто этого не использует. Шаред стейт идет или из замыканий или из примесей в объекты (в те же Request и Response). А про GoF паттерн «цепочка ответственности» в мире ноды мало кто знает и не проводят параллели с мидлварами. А основная идея «цепочки ответственности» в том, что только одно звено должно мутировать состояние, передавая управление другу другу по цепочке, они и выясняют, чья же ответственность в этом случае, но менять состояние может только тот, кто принял на себя ответственность. А в мидлварах не так сделано, все все мутируют, а еще и события навешивают, потом управление уже ушло дальше, а события их догоняют, происходит коррапшен данных, а в сокет пишут из разных мест приложения. Когда все ломается, то люди переставляют мидлвары местами, заплатки ставят, но это все не стабильно, это все идет из того, что мидлвары это антипаттерн и это должны узнать и признать все.
                                            +2

                                            Я смотрю Ваши видео и всячески их пропагандирую. Спасибо Вам огромное, за пару месяцев я закрыл столько темных пятен а js, сколько не закрыл на курсах на которые потратил более двух лет.

                                        0

                                        Немного уточню по graphql. Я не воспринимаю его как монолит который общается с базой данных. Как раз resolver в graphql может брать данные откуда угодно. В тосхм числе из базы данных напрямую, из других микросервисов, такой себе роутер.

                                          0
                                          Конечно. Вопрос в том, кто будет писать резолверы — бэкендер или фронтендер.
                                            0

                                            Если фронтенда разработчики пишут запросы на axios к микросервисов. То почему они не могут писать такие же запросы на том же axios к тем же микросервисов но только из резольверов. Это же не более мудрено чем тот же redux

                                              0
                                              Я именно про такую схему и упоминаю в посте (BFF с GQL), в противовес схеме, когда с бэкенда торчит GQL API, и фронтам нет доступа к резолверам.

                                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                        Самое читаемое