Чистое API на Node.js: практическое руководство
Собираем минимальный, но продакшен-готовый скелет на Express и показываем, как те же принципы работают в Fastify и NestJS.
Если ваше Node.js-приложение начиналось с одного server.js, а через полгода превратилось в лабиринт маршрутов, где бизнес-логика растворилась в обработчиках Express — эта статья для вас. Чистое API проектируется не ради красоты, а чтобы команда могла добавлять конечные точки, не боясь сломать соседние.
Разберём минимальный, но готовый к продакшену скелет: разделение слоёв, валидацию входных данных, централизованную обработку ошибок, версионирование, ограничение частоты запросов и автоматическую документацию. Все принципы применимы и к Express, и к Fastify, и к NestJS.
Чистое API — это прежде всего разделение ответственности: роутер знает маршруты, контроллер переводит HTTP в вызовы сервиса, сервис отвечает за бизнес-логику, а схема проверяет входные данные. Такой подход делает код предсказуемым при любом масштабе.
Ключевые выводы
Чистое API — это, прежде всего, разделение ответственности: роутеры, контроллеры, сервисы и валидация живут в разных слоях.
Валидация с помощью Zod выполняется на границе — до того, как запрос попадёт в бизнес-логику.
Централизованный обработчик ошибок — единственное место, где исключения превращаются в HTTP-ответы.
Стоит закладывать версионирование и документацию OpenAPI с первого дня: позже это обойдётся дороже.
Fastify и NestJS дают те же архитектурные идеи из коробки, но логика слоёв от этого не меняется.
Что мы будем строить
Возьмём намеренно простой домен — каталог товаров. Нам важна не бизнес-логика, а структура. К концу у нас будет REST API с единым форматом ответов, валидацией, версионированием по пути /api/v1, ограничением частоты запросов и интерактивной документацией Swagger.
Структура проекта
Прежде чем писать код, договоримся, где что лежит. Каждая фича — отдельная папка, а не рассыпанный по проекту набор файлов.
- api/v1/ — все маршруты версионированы с первого дня. Добавить v2 позже можно новой папкой, а не рефакторингом.
- products/ — фичевая папка владеет роутером, контроллером, сервисом и схемой.
- middleware/ — сквозная функциональность: защита, логирование, лимиты.
- lib/ — утилиты без привязки к фреймворку.
Разделяем ответственность
Самая частая ошибка в Express-приложениях — бизнес-логика внутри обработчика маршрута. Там же появляется валидация, работа с базой данных и формирование ответа. При росте проекта такой файл становится опасным для изменений.
Роутер знает только «куда идти»
Промежуточный обработчик валидации
Промежуточный обработчик validateRequest проверяет body, params и query до попадания в контроллер. Если данные не проходят проверку, ошибка передаётся в централизованный обработчик.
Контроллер переводит HTTP в вызовы сервиса
Контроллер не знает, где хранятся товары. Его задача — извлечь параметры из запроса, вызвать сервис и вернуть ответ. Всё остальное передаётся в централизованный обработчик ошибок через next(error).
Сервис содержит бизнес-логику
Сервис не зависит от HTTP. Когда придёт время заменить хранилище в памяти на настоящую базу данных, потребуется поправить только этот файл.
Валидация на границе с Zod
Любое API, принимающее внешние данные, должно их проверять. Без валидации один некорректный запрос способен превратиться в ошибку времени выполнения, некорректную запись в базе или уязвимость.
Zod даёт две вещи сразу: проверку во время выполнения и типы TypeScript, выведенные из одной схемы. Промежуточный обработчик validateRequest проверяет body, params и query до того, как запрос попадёт в контроллер. Если данные невалидны, дальше они не идут.
Единый обработчик ошибок
Разбросанная обработка ошибок — один из главных источников хаоса: где-то возвращается { error: '...' }, где-то { message: '...' }, а где-то случайно отдаётся HTML-страница. Решение — один обработчик, через который проходят все исключения.
Теперь клиент всегда получает предсказуемую форму ответа, а добавление логирования или отправки ошибок в мониторинг — однострочное изменение в одном месте.
Единый формат ответов
Успешный ответ всегда выглядит как { success: true, data: ... }, а ошибка — как { success: false, error: { code, message, details } }. Фронтенд или сторонний интегратор знает, чего ожидать от любого эндпоинта.
Версионирование API
Версионировать API с первого дня стоит недорого. Добавить версию позже — значит ломать существующих клиентов или городить сложную миграцию.
Новая версия — новая папка src/api/v2/ и новый префикс. Старые клиенты продолжают работать на /api/v1.
Ограничение частоты запросов
Rate limiting защищает API от случайных и намеренных перегрузок. Настроить его в Express помогает пакет express-rate-limit.
Глобальный лимит распространяется на все запросы; операции, которые изменяют данные, ограничены жёстче. Ответ тоже соответствует единому формату ошибки.
Документация OpenAPI и Swagger
API без документации годится только для автора. С помощью swagger-jsdoc и swagger-ui-express можно получить интерактивную документацию прямо из JSDoc-комментариев в роутерах.
Совет:
Держите описания эндпоинтов в одном файле с маршрутами, а glob в swagger.ts настройте на файлы, доступные во время работы приложения. Лучше генерировать спецификацию на этапе сборки, чем полагаться на исходники TypeScript.
Fastify и NestJS: альтернативы Express
Всё, что мы разобрали, работает и в Express. Но если вы начинаете проект с нуля, стоит взглянуть на альтернативы.
- Fastify — быстрее Express в бенчмарках (порой в два раза), имеет встроенный логгер Pino и валидацию по схеме. Разделение слоёв остаётся на совести разработчика.
- NestJS — популярен в крупных компаниях: слои модулей, контроллеров и сервисов навязаны архитектурой, что упрощает введение новых разработчиков в проект.
- Express — остаётся лучшим выбором, если вы присоединяетесь к существующему проекту или команда уже знает экосистему.
Архитектурные принципы — разделение слоёв, единый формат ошибок, валидация на границе — не зависят от фреймворка. Меняется только синтаксис.
FAQ
Часто задаваемые вопросы
Что такое «чистое API»?
Это API, в котором слои разделены: роутер за маршруты, контроллер — за HTTP, сервис — за бизнес-логику, схема — за валидацию.
Зачем отделять контроллеры от сервисов?
Контроллер знает о запросе и ответе, сервис — нет. Благодаря этому бизнес-логику можно тестировать без HTTP-сервера, а при смене фреймворка переписывать только тонкий слой контроллеров.
Почему валидация должна идти до бизнес-логики?
Ранняя валидация снижает риск многих атак, включая некоторые виды инъекций, но не заменяет параметризованные запросы, ORM и безопасную работу с базой данных.
Стоит ли сразу версионировать API?
Да. Добавить /api/v1 сегодня стоит одной строки. Переехать на версионирование после появления первых клиентов обходится гораздо дороже.
Когда выбрать Fastify или NestJS вместо Express?
Fastify — если важна производительность и TypeScript сразу. NestJS — если нужна жёсткая архитектурная дисциплина для растущей команды. Express — для существующих проектов и знакомого стека.
Минимальный набор для старта
Для самостоятельного запуска понадобятся базовые зависимости и алиасы путей в tsconfig.json. Объявите @/* на папку src, и примеры заработают без ручных правок импортов.
Выводы
Чистая структура API — это не переусложнение. Это минимум, при котором бэкенд можно поддерживать нескольким людям.
Мы собрали минимальный, но масштабируемый каркас: папки по фичам, разделённые слои, валидацию на границе, централизованную обработку ошибок, единый формат ответов, версионирование, лимиты и автоматическую документацию. Ни один из этих шагов сложный сам по себе. Их ценность — в сочетании и в том, чтобы сделать всё это до того, как код разрастётся.
Если начинаете новый Node.js-проект, не откладывайте структуру «на потом». А в существующем — попробуйте вынести валидацию в схему и собрать ошибки в одном обработчике. Потом всегда дороже.
Источники
Идеи и примеры в статье основаны на материале Gavin Cettolo «Clean API Design in Node.js: A Practical Guide» (dev.to).