Чистое API на Node.js: практическое руководство

Собираем минимальный, но продакшен-готовый скелет на Express и показываем, как те же принципы работают в Fastify и NestJS.

Обложка: Чистое API на Node.js: практическое руководство

Если ваше Node.js-приложение начиналось с одного server.js, а через полгода превратилось в лабиринт маршрутов, где бизнес-логика растворилась в обработчиках Express — эта статья для вас. Чистое API проектируется не ради красоты, а чтобы команда могла добавлять конечные точки, не боясь сломать соседние.

Разберём минимальный, но готовый к продакшену скелет: разделение слоёв, валидацию входных данных, централизованную обработку ошибок, версионирование, ограничение частоты запросов и автоматическую документацию. Все принципы применимы и к Express, и к Fastify, и к NestJS.

Чистое API — это прежде всего разделение ответственности: роутер знает маршруты, контроллер переводит HTTP в вызовы сервиса, сервис отвечает за бизнес-логику, а схема проверяет входные данные. Такой подход делает код предсказуемым при любом масштабе.

Ключевые выводы

Чистое API — это, прежде всего, разделение ответственности: роутеры, контроллеры, сервисы и валидация живут в разных слоях.

Валидация с помощью Zod выполняется на границе — до того, как запрос попадёт в бизнес-логику.

Централизованный обработчик ошибок — единственное место, где исключения превращаются в HTTP-ответы.

Стоит закладывать версионирование и документацию OpenAPI с первого дня: позже это обойдётся дороже.

Fastify и NestJS дают те же архитектурные идеи из коробки, но логика слоёв от этого не меняется.

Что мы будем строить

Возьмём намеренно простой домен — каталог товаров. Нам важна не бизнес-логика, а структура. К концу у нас будет REST API с единым форматом ответов, валидацией, версионированием по пути /api/v1, ограничением частоты запросов и интерактивной документацией Swagger.

Структура проекта

Прежде чем писать код, договоримся, где что лежит. Каждая фича — отдельная папка, а не рассыпанный по проекту набор файлов.

			src/
├── api/v1/
│   └── products/
│       ├── products.router.ts
│       ├── products.controller.ts
│       ├── products.service.ts
│       └── products.schema.ts
├── middleware/
│   ├── errorHandler.ts
│   ├── rateLimiter.ts
│   └── validateRequest.ts
├── lib/
│   ├── AppError.ts
│   └── responseHelper.ts
├── app.ts
└── server.ts
		
  • 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

Часто задаваемые вопросы
1
Что такое «чистое API»?

Это API, в котором слои разделены: роутер за маршруты, контроллер — за HTTP, сервис — за бизнес-логику, схема — за валидацию.

2
Зачем отделять контроллеры от сервисов?

Контроллер знает о запросе и ответе, сервис — нет. Благодаря этому бизнес-логику можно тестировать без HTTP-сервера, а при смене фреймворка переписывать только тонкий слой контроллеров.

3
Почему валидация должна идти до бизнес-логики?

Ранняя валидация снижает риск многих атак, включая некоторые виды инъекций, но не заменяет параметризованные запросы, ORM и безопасную работу с базой данных.

4
Стоит ли сразу версионировать API?

Да. Добавить /api/v1 сегодня стоит одной строки. Переехать на версионирование после появления первых клиентов обходится гораздо дороже.

5
Когда выбрать Fastify или NestJS вместо Express?

Fastify — если важна производительность и TypeScript сразу. NestJS — если нужна жёсткая архитектурная дисциплина для растущей команды. Express — для существующих проектов и знакомого стека.

Минимальный набор для старта

Для самостоятельного запуска понадобятся базовые зависимости и алиасы путей в tsconfig.json. Объявите @/* на папку src, и примеры заработают без ручных правок импортов.

			// package.json — основные зависимости
{
  "dependencies": {
    "express": "^4.21.0",
    "zod": "^3.23.0",
    "express-rate-limit": "^7.4.0",
    "swagger-jsdoc": "^6.2.8",
    "swagger-ui-express": "^5.0.1"
  },
  "devDependencies": {
    "@types/express": "^4.17.21",
    "typescript": "^5.6.0"
  }
}

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./",
    "paths": { "@/*": ["src/*"] }
  }
}
		

Выводы

Чистая структура API — это не переусложнение. Это минимум, при котором бэкенд можно поддерживать нескольким людям.
Gavin CettoloFullstack Product & Platform Engineer

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

Если начинаете новый Node.js-проект, не откладывайте структуру «на потом». А в существующем — попробуйте вынести валидацию в схему и собрать ошибки в одном обработчике. Потом всегда дороже.

Источники

Идеи и примеры в статье основаны на материале Gavin Cettolo «Clean API Design in Node.js: A Practical Guide» (dev.to).