Next.js изнутри. Часть 5. Серверный слой.
Введение
В прошлых частях мы разобрали Pages и App Router, но весь этот разговор начинался уже внутри рендеринга. На самом деле до рендера запрос проходит через целый слой, который мы лишь мельком упомянули в первой части: матчинг маршрута, middleware, проверку файловой системы, отдачу статики.
В этой части мы посмотрим на этот слой. Проследим путь запроса от момента, когда он пришёл на порт, до момента, когда вызывается рендер. Главное, что стоит держать в голове с самого начала: next start поднимает два логических слоя. Один отвечает за маршрутизацию, второй — за рендеринг.
Колбэк на HTTP-сервере
В основе своей Next.js не делает ничего экзотического с сетью. Когда вы запускаете next start, фреймворк создаёт обычный Node.js HTTP-сервер и навешивает на него один обработчик запросов. Всё, что Next.js умеет — роутинг, рендеринг, кеширование — это то, что происходит внутри этого обработчика. Снаружи это просто функция (req, res), как в любом сервере на голом Node.
Из этого следует и то, как Next.js встраивается в чужую инфраструктуру: если у вас уже есть HTTP-сервер, Next.js может отдать вам свой обработчик, и вы навесите его сами.
Любопытная деталь старта: сервер начинает слушать порт раньше, чем готова вся внутренняя машинерия. Запросы, которые прилетели в этот момент, не отбрасываются, а дожидаются, пока инициализация завершится, и только потом обрабатываются. Это сделано ради того, чтобы сокет открывался как можно быстрее и ни один ранний запрос не потерялся.
Два слоя: router-server и render-server
Теперь к главному. Запустив HTTP-сервер, Next.js поднимает над ним два слоя, у которых принципиально разные задачи.
Первый слой router-server — это маршрутизатор. Он принимает сырой запрос и решает, что с ним делать: применить редирект из конфига, прогнать через middleware, переписать URL по правилу rewrite, отдать статический файл с диска или, если речь о настоящей странице, передать запрос дальше — на рендеринг. Сам router-server ничего не рендерит. Он не знает про React, RSC и гидратацию.
Второй слой — render-server. Это тот самый сервер, который мы неявно обсуждали в предыдущих частях: он берёт разрешённый маршрут и превращает его в HTML или Flight-поток. Внутри render-server живёт BaseServer и его реализация для Node.js, про которые шла речь в первой части.
Дальше мы рассмотрим подробнее router-server — именно там происходит всё интересное до рендера.
Порядок обработки запроса
Когда запрос попадает в router-server, он прогоняется через фиксированную последовательность шагов, которую можно сложить в такую лестницу:
- Заголовки из
next.config— добавляются к ответу. - Редиректы из конфига — если совпало, запрос разворачивается.
- Middleware — если URL подходит под матчер, запускается наш код.
- Rewrites группы
beforeFilesизnext.config— переписывание URL до проверки файлов. - Проверка файловой системы — точное совпадение с реальным маршрутом или статическим файлом.
- Rewrites группы
afterFilesизnext.config— переписывание URL, если по файлам ничего не нашлось. - Динамические маршруты и повторная проверка.
- Rewrites группы
fallbackизnext.config— последний шанс переписать URL.
Этот порядок объясняет целый класс вопросов, например, почему middleware видит запрос раньше, чем срабатывает редирект из вашего кода внутри страницы.
В минимальном режиме — так Next.js работает на serverless-платформах, где маршрутизацию берёт на себя сама платформа — почти все эти шаги отключены: редиректы, заголовки и rewrites уже применены инфраструктурой платформы. Router-server в этом режиме занимается в основном тем, что доводит запрос до рендера.
Что сервер отдаёт сам
Важный момент: далеко не каждый запрос доходит до render-server. На шаге проверки файловой системы router-server сверяет путь с тем, что у него есть на диске, и для целого ряда случаев отвечает сам, не запуская рендеринг вообще.
Чтобы понимать, что есть на диске, на старте router-server считывает служебную информацию о сборке: идентификатор сборки, списки страниц и роут-хендлеров, содержимое папки public, правила для middleware. Для всего этого сервер использует для этого уже знакомые нам манифесты.
Существует развилка по типу совпадения:
- Запрос к
/_next/static/...— это собранные при билде чанки. Они отдаются как статические файлы напрямую, без какого-либо участия React. - Запрос к файлу из папки
public— тоже отдаётся как статика. - Запрос к оптимизируемой картинке уходит в оптимизатор изображений.
- И только запрос, который совпал с настоящей страницей или роут-хендлером, передаётся дальше — в рендеринг.
API-эндпоинты
До сих пор мы говорили про то, что Next.js делает сам. Но часть маршрутов разработчик описывает руками — это API-эндпоинты. Тут полезно понять, как наш код подключается к слою, который мы только что разобрали. В Next.js есть две модели написания эндпоинтов, и они довольно разные.
Route Handlers — современная модель App Router. Мы создаём файл route.ts и экспортируем из него функции, названные по HTTP-методам: GET, POST, DELETE и так далее. На сборке Next.js оборачивает этот файл в служебный модуль, который собирает из наших экспортов таблицу «метод → функция». Когда приходит запрос, модуль смотрит на его HTTP-метод и вызывает соответствующий обработчик.
API Routes — старая модель Pages Router. Здесь мы экспортируем один обработчик по умолчанию — функцию (req, res). Next.js прогоняет её через свою обвязку, которая досыпает в req и res удобные хелперы: разбор тела запроса, чтение кук, методы вроде res.json(). Это классический Node-стиль: мы получаем запрос и ответ Node.js, слегка обогащённые, и сами пишем в ответ.
С точки зрения router-server API-эндпоинт и страница — это один и тот же тип «выхода»: динамический маршрут, который надо передать в render-server. Просто эндпоинт рендерится не в HTML, а в ответ, который мы сформировали руками.
Middleware (proxy)
Вторая точка, где код разработчика врезается в серверный слой — это middleware (в свежих версиях его постепенно переименовывают в proxy). Мы описываем в нём правило — для каких путей он должен срабатывать. На сборке это правило превращается в набор регулярных выражений, которые ложатся в служебную карту (middleware-manifest). В рантайме на третьем шаге router-server сверяет путь запроса с этими выражениями, и если совпало — запускает наш код. Правило может быть и сложнее простого совпадения по пути: можно требовать наличия определённого заголовка или куки, либо наоборот их отсутствия.
Дальше наша функция получает запрос и возвращает ответ: «пропустить дальше», «переписать URL», «сделать редирект», «добавить заголовок». Но middleware не вызывает маршрутизатор напрямую. Вместо этого его решение кодируется в специальные служебные заголовки ответа.
Router-server читает эти служебные заголовки обратно и поступает согласно им: разворачивает редирект, подменяет путь, прокидывает изменённые заголовки в следующий шаг.
У middleware есть и важное ограничение: он всегда исполняется на Edge, а не в полноценном Node.js. Причина в его роли: middleware стоит на пути всех запросов, подходящих под его правило, ещё до того, как стало понятно, что вообще с запросом делать. На таком горячем участке тяжёлая среда исполнения бы сильно резала производительность.
Передача в рендер
Допустим, запрос прошёл всю лестницу: его не развернул редирект, middleware пропустил дальше, по файловой системе он совпал с настоящей страницей. Теперь router-server передаёт его в render-server.
Router-server уже проделал всю работу по разрешению маршрута: он знает финальный путь и параметры запроса. Эту разрешённую информацию он добавляет как служебные метаданные и зовёт обработчик render-server. Тот берёт готовый разрешённый маршрут и сразу идёт в пайплайн рендера, тот самый, что мы разбирали в предыдущих частях.
При этом render-server — это не финальная инстанция. Router-server вызывает его так же, как вызывал любой другой шаг лестницы. И если render-server возвращает «этот путь отрендерить не удалось» (например, для динамического маршрута без предрендера и без fallback), router-server не считает это концом: последнее слово остаётся за ним, и он просто продолжает лестницу со следующего шага, пробуя подобрать другой выход.
Итого
Серверный слой Next.js — это два слоя с разделением труда. Router-server занимается маршрутизацией: прогоняет запрос через фиксированную лестницу из заголовков, редиректов, middleware, rewrites и проверки файловой системы; статику, ассеты и картинки отдаёт сам; а до рендера доводит только то, что действительно нужно рендерить. Render-server — отдельный слой, который берёт уже разрешённый маршрут и превращает его в ответ. Код разработчика встраивается в эту трубу в двух местах: эндпоинты и middleware.