Это вторая часть статьи о применении комбинации технологий nest.js и NEXT.js. В первой части был создан и настроен проект, а также выбран способ отправки данных для SSR, в результате чего проект уже удовлетворял большинство потребностей при разработке простого сайта. В этой статье можно узнать о том, как выжать максимум пользы из nest-next: HMR, CDN, удобный SSR и разворачивание "за слешом". Вступление Разворачивание на CDN HMR и кэширование инстанса сервера NEXT.js Подключение HMR в nest Кэширование инстанса сервера NEXT.js  Передача данных клиенту при SSR Создание конфига AOP для GSSP Доступ к контексту приложения  Доступ к частям контекста Работа клиентских переходов Разворачивание "за слешом" Создание proxy для разработки Добавление basePath в конфигурацию Proxy и basePath  Обертка над fetch Заключение В данной статье можно узнать про продвинутые механики в nest-next и про то, как их можно применять с пользой. Напомню, что в первой части статьи уже был создан и настроен проект, поверх которого будут производиться изменения здесь. Готовое приложение доступно в репозитории на GitHub - https://github.com/yakovlev-alexey/nest-next-example - последовательность коммитов в целом совпадает с ходом статьи.  Необходимая для начала этой статьи ревизия доступна по ссылке. Начнем с простого. NEXT.js "из коробки" поддерживает разворачивание на CDN. Для этого достаточно добавить assetPrefix в конфигурацию next.config.js при сборке. После ее завершения нужно загрузить содержимое ./.next/static на CDN - сервер NEXT.js автоматически будет подключать нужные ресурсы оттуда. Важным моментом будет то, что assetPrefix должен присутствовать в конфигурации и при запуске продакшен сервера nest. В текущий момент приложение в режиме разработки работает быстро, перекомпиляция скорее всего занимает не более 3 секунд, а страницы грузятся почти мгновенно. Здорово, если сборка будет оставаться такой же быстрой по мере развития проекта, но это маловероятно. Особенно неприятной проблемой может стать раздутый клиент: при каждом перезапуске сервера nest (при изменении любого файла на сервере) создается новый сервер NEXT.js, который заново собирает клиентский бандл. Решение простое: нужно подключить HMR в nest и кэшировать инстанс сервера NEXT.js. Начнем по порядку. Проследуем официальному рецепту по добавлению Hot Reload в nest из документации. Я не буду копировать в статью все содержимое рецепта, но обращу внимание на некоторые детали.  Так, нам не сгодится имеющаяся команда запуска сервера, так как она будет пытаться использовать базовый tsconfig.json, в то время как мы хотим использовать tsconfig.server.json. Отразим это в команде запуска: Следует обратить внимание на то, что название передается в сыром виде: ts-loader в таком случае будет искать этот файл рекурсивно в родительских папках. Если бы мы передали относительный путь, то пришлось бы указывать его относительно исполняемого файла сервера. Далее взглянем на подключение Hot Reload в main.ts.  Здесь все в порядке, но обратим внимание на то, как мы обрабатываем очистку при завершении работы старого модуля: мы вызываем метод app.close() - тем самым полностью закрываем все приложения и все сервисы. Проверим работу сервера после подключения HMR. Запустим сервер и попробуем изменить что-то в app.controller.ts. Важным моментом является то, что сервер не перезапустится, если в результате компиляции не было обнаружено изменений. Если же изменениия были, то время запуска сервера должно упасть до ~1-2 секунд в маленьких проектах и не более 4-5 секунд в больших - существенная разница, которая сильно улучшает качество жизни разработчикам. Если какие-то модули на сервере nest использовали динамический import/require, то велика вероятность, что после подключения Webpack HMR этот код перестанет работать из-за бандлинга. В таком случае, есть вариант запускать компилятор TypeScript параллельно с работой Webpack nest. Результаты компиляции лучше класть в отдельную папку (например, build) и указать при запуске сервера переменную окружения NODE_PATH=./build, чтобы динамический импорт модулей искал их в нужном месте. Тем не менее, у нас остается большой ботлнек при перезапуске в виде пересоздания сервера NEXT.js. В разросшихся проектах это может занимать несколько секунд при инициализации сервера и десятки секунд при первой загрузке страницы - не хочется ждать такое время просто из-за изменения символа в каком-то контроллере на сервере nest. Чтобы побороть эту проблему, будем кэшировать RenderModule из nest-next между перезагрузками модуля app.module.ts. Для этого сменим инициализацию AppModule на динамическую.  Обновим main.ts: теперь для подключения AppModule нам нужно вызывать метод initialize(). Чтобы убедиться в корректной работе наших улучшений, последим за выводом в терминал при перезагрузке сервера nest. В случае успеха мы больше не увидим надпись compiling... при перезагрузках - это значит, что кэш сборки NEXT.js не сбрасывается и мы выиграли еще больше времени для разработки без ожидания сборки. Изначально перед нами стоял вопрос: как с наименьшими потерями избавиться от необходимости поднимать два сервиса для работы nest и NEXT.js - кажется, удалось избавиться от всех неудобств, которые могла принести провязка nest-next. Теперь же хочется получить реальные преимущества от такого взаимодействия. Ранее мы отказались от варианта передачи данных, необходимых для страницы, на клиент с помощью возвращаемого значения функции-контроллера. Мы также обнаружили, что с помощью интерсепторов мы можем удобным образом подкладывать данные во все запросы на страницы в nest, не ломая клиентские переходы. В голову приходит мысль: а почему бы не использовать интерсепторы, чтобы помещать на клиент какие-то общие данные, необходимые для всех страниц? Это могут быть различные конфигурации, флаги, переводы и так далее. Действительно, давайте этим займемся. Для начала создадим простенький конфиг - не будем разрабатывать отдельные модуль или сервис nest, а обойдемся простым файлом. В конфиг поместим фичефлаги, которые будем обрабатывать на клиенте. В качестве первого фичефлага возьмем blog_link - будем менять отображение ссылки на блог на главной странице в зависимости от состояния этого флага. Создадим интерсептор ConfigInterceptor для подкладывания конфига в возвращаемое значение контроллеров и подключим его. Для проверки работы интерсептора можем добавить console.log(ctx.query) в GSSP. Не следует помещать в возвращаемое значение скрытую информацию вроде токенов, адресов и так далее - ctx.query так же сериализуется и при отправке на клиент. Следовательно, не рекомендуется отправлять через ctx.query в GSSP NEXT.js в том числе и объемные данные - для этого можно использовать объект запроса req из Express. Теперь необходимо добавить одну и ту же обработку для всех методов GSSP - конфиг нужно поместить в возвращаемое значение этого метода. Создадим обертку buildServerSideProps для этого. Как можно заметить, для удобства мы получаем в параметры не полноценный метод GSSP, а его упрощенную версию, которая возвращает сразу поле props, не вложенное в объект. Это лишает нас возможности возвращать поле redirect. Если возникает такая необходимость, поменяйте метод buildServerSideProps соответствующим образом. Для корректной типизации нам потребовались тайпинги для нашего конфига, добавим их. Также нас не устраивает встроенный в NEXT.js тип GetServerSideProps - нельзя перезаписать тип Query, он всегда должен удовлетворять требованию ParsedUrlQuery. Добавим собственные тайпинги. Обновим наши страницы, чтобы они использовали эту обертку. Важно понимать, что не следует отправлять на клиент всю конфигурацию сервера. Там может содержаться скрытая информация (токены, адреса, креды), а даже если такой информации нет, то размер загружаемой страницы будет увеличиваться от наличия неиспользуемых данных. Уже сейчас мы можем получить доступ к features из наших страниц. Но это может быть неудобно - чтобы получить доступ к данным о фиче-флаге или другой конфигурации в каком-то компоненте, нам придется применить prop-drilling. Чтобы этого избежать, создадим контекст для данных приложения. Чтобы не повторять подключение контекста в каждом экране, вынесем эту логику в компонент _app.tsx. Для удобства я реализую этот компонент через класс, но это необязательно. Немного поменяем buildServerSideProps: И вынесем доступ к AppDataContext в отдельный хук. Наконец, реализуем хук useFeature и применим его на Home странице. Проверим в браузере: теперь по адресу localhost:3000 мы должны увидеть измененный формат ссылки - надпись Link рядом будет ссылкой, в то время как название просто текстом. Изменим значение в конфиге, дождемся перезапуска сервера и повторим эксперимент - теперь мы видим старый формат списка. Внимательный разработчик после добавления такой возможности попробует осуществить клиентский переход: ведь мы сильно поменяли метод GSSP, нужно проверить его работу. Опасения подтвердятся - браузер перезагружает страницу при переходе, а в терминале сервера ошибка: невозможно сериализовать поле appData.features - оно ожидаемо приходит undefined при клиентском переходе. Связано это с тем, что контроллер nest не вызывается, соответственно, интерсептор не подкладывает значение конфига в GSSP. Применяйте такое прокидывание данных в GSSP только для SSR, чтобы разово разместить данные на клиенте, а в методах GSSP не закладывайтесь на их присутствие в запросе. Такими данными могут быть фиче-флаги, переводы, прочие конфиги.  Чтобы избавиться от такой ошибки, обернем возвращаемое значение buildServerSideProps в рекурсивный метод очистки несериализуемых значений. Убедимся в работе переходов - действительно, теперь все в порядке. Возможность легко передавать на клиент арбитрарные данные для работы приложения является одним из самых сильных преимуществ при работе с nest-next. При этом источником этих данных может служить что угодно, в том числе значения, которые express-middleware подкладывает в req, что может быть удобно при разработке в энтерпрайзе с уже имеющимися решениями. Изменения в фиче-флагах/переводах/конфигурации, вносимые в CMS, могут быть отображены на клиенте почти мгновенно. Предположим, что разрабатываемый сервис в продакшене разворачивается "за слешом". Например, мы делаем документацию для какого-то проекта и сервис будет находится за прокси с адресом /docs. Или, перекладывая на текущее приложение - отдел с блогом - нашим префиксом будет /blog. Что нужно сделать, чтобы поддержать такую возможность? Необходимо добавить префикс во все переходы (ссылки), а также к запросам к серверу, но только с клиента (не в GSSP). Статика будет лежать на CDN, для нас не будет это проблемой. Кажется, все это в наших силах, у нас даже есть механизм подкладывания данных на клиент при SSR. Но тут мы вспоминаем, что NEXT.js при переходах делает запрос во внутренний эндпоинт, который исполняет GSSP на сервере и возвращает сериализованные данные для следующей страницы. На этом наши полномочия заканчиваются, клиентские переходы будут неизбежно сломаны. А если мы не будем пользоваться CDN, то сломается и вся статика - это совершенно не годится. В документации NEXT.js мы обнаружим параметр basePath, который "из коробки" добавляет всем клиентским переходам необходимый префикс, а также добавляет его во все запросы внутренним эндпоинтам сервера NEXT.js. Отлично, план есть, приступаем. Прежде чем мы начнем добавлять поддержку basePath в проект, нам следует написать простенький proxy-сервер, чтобы проверить работу нашего сайта. Воспользуемся Docker и nginx. Создадим нужный конфиг для nginx. Важно понимать, что proxy будет запускаться в Docker-контейнере с собственной сетью, поэтому для проксирования запроса к нашему серверу нам потребуется адрес хост-машины: будем помещать его в контейнер через имя localnode. В скрипты в package.json добавим скрипт start:proxy для запуска контейнера со следующей командой: Добавим поддержку basePath в конфигурацию сервера. Обновим типы и будем брать значение из переменной окружения BASE_PATH. Создадим next.config.js - файл конфигурации NEXT.js. Поместим туда такое же поле. Проверим работу сервиса. Перезапустим сервер разработки и вновь зайдем на localhost:8080/blog. Снова запрос доходит до сервера, но не удается запросить статику NEXT.js. Сервер NEXT.js ожидает, что запрос придет с соответствующим basePath в начале req.url. Мы же при проксировании отрезаем эту часть запроса. Добавим отдельное правило для проксирования запросов на /blog/_next без перезаписи адреса запроса. Перезапускаем контейнер Docker и снова проверяем сервис в браузере. К большому сожалению, опять не работает. Здесь мы встречаем ограничение реализации модуля nest-next. Проблема связана с тем, что nest-next устанавливает фильтр nest, который перенаправляет необработанные контроллерами запросы обработчикам NEXT.js. В исходном коде фильтра вызывается обработчик ошибки во всех запросах, которые не начинаются на /_next. Получается, сервер NEXT.js ожидает запрос, начинающийся на basePath, а nest-next проксирует только запросы, начинающиеся на /_next. Мною был открыт PR на добавление поддержки basePath в nest-next. Он был вмерджен автором пакета, но новая версия не была собрана. Пока нет версии, можно загрузить собранную версию с GitHub следующим способом. Этот тег base-path-dist содержит в себе собранный пакет только с необходимыми файлами. После обновления версии пакета проверим в браузере вебсайт: наконец-то мы видим знакомую страницу Home уже по адресу localhost:8080/blog. Проверим переход - тоже работает! Остается только добавлять basePath к запросам из fetch. Сейчас может показаться, что в этом нет необходимости, можно просто использовать уже имеющуюся обертку над fetch в GSSP, а в остальных случаях обычный fetch. Но что, если логика запросов находится в стейт-менеджере и может вызываться как на сервере, так и на клиенте? В таком случае нам действительно нужно в зависимости от среды исполнения выполнять дополнительные проверки на адрес запроса. Первым делом, немного отрефакторим buildServerSideProps. Обновим тайпинги для AppData и вынесем метод по извлечению appData из ctx.query в отдельный метод. Подключим новый хелпер в buildServerSideProps. Наконец, у нас есть доступ к basePath на клиенте. Осталось добавить поддержку этого значения в fetch. Я не буду заморачиваться с хитрым решением, а просто превращу наш envAwareFetch из чистой функции в функцию с побочными эффектами. Отобразим изменения в fetch.ts. Остается только инициализировать fetch с помощью initializeFetch. Кажется, что это можно было бы сделать в GSSP, но этот метод исполняется только на сервере, а нам необходимо добавлять basePath как раз на клиенте, поэтому в качестве места вызова метода я выбрал конструктор _app.tsx. Чтобы проверить, можем добавить запрос в эффект на клиенте в index.tsx. Таким образом можно задеплоить nest-next приложение "за слешом", не теряя преимуществ ни одного из фреймворков. Работая в симбиозе, nest и NEXT.js являются очень сильным инструментом разработки веб-приложений и сервисов. Надеюсь, что данная статья поможет желающим попробовать эти фреймворки вместе войти в разработку с максимальной эффективностью несмотря на скудность официальной документации nest-next. Также надеюсь, что и опытные пользователи этой комбинации инструментов нашли для себя в статье что-то новое. Разработчик