28 июл. 2026 г.

Nest REST Backend Template: backend-шаблон, с которого я начинаю новые проекты

Production-oriented NestJS шаблон для быстрого старта backend-проектов: Fastify, Drizzle ORM, PostgreSQL, CQRS, RBAC/CASL, gRPC auth-service, JWT/cookies, S3-файлы, BullMQ, i18n, captcha, healthchecks, Docker и observability.

TypeScriptNode.jsNestJSFastifyPostgreSQLRedisDocker ComposeS3MinIOBullMQREST APISwagger / OpenAPIDrizzle ORMCQRSClean ArchitectureDDDCASLJWT

nest-rest-backend-template - мой личный backend-шаблон, с которого я начинаю новые проекты. Это не заготовка уровня “Nest CLI + UsersController”. Я собрал его как стартовую точку для продуктовых систем, где почти всегда нужны одни и те же базовые вещи: пользователи, роли, авторизация, файлы, очереди, письма, уведомления, healthchecks, Docker, конфигурация, миграции и понятная архитектурная карта.

Мне не хотелось каждый раз заново вспоминать, как правильно подключить Fastify, Pino, Swagger, CSRF, S3, Redis, BullMQ, Drizzle, health endpoints и seed админа. Еще меньше хотелось копировать куски из старых проектов и потом неделями разгребать отличия. Поэтому шаблон стал для меня не “boilerplate ради boilerplate”, а способом начинать проекты с уже продуманной инженерной базы.

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

Что внутри

Шаблон построен как Nest CLI monorepo с двумя deployable apps:

  • apps/backend - основной REST API на NestJS/Fastify;
  • apps/auth-service - отдельный gRPC-сервис для credential verification, token issuance, refresh rotation и password flows;
  • libs/ - общий слой для контрактов, auth-типов, password service, Drizzle-схемы auth-таблиц и общего config loader.

В основном backend уже есть несколько модулей, которые я часто использую как основу:

  • users - пользователи, роли, permissions, auth HTTP surface;
  • file - S3/MinIO backed file storage с версиями файлов;
  • mail - письма через Handlebars templates и BullMQ queue;
  • notification - in-app уведомления с асинхронной dispatch-моделью;
  • captcha - собственная SVG captcha с pre-generated pool в Redis и asset storage в S3;
  • admin - dashboard, access logs, system settings;
  • health - liveness/readiness endpoints;
  • migration - boot-time seeding ролей, permissions, админа и шаблонов.

Это много для “шаблона”, но в реальных backend-проектах именно эти куски часто появляются уже в первые недели. Разница в том, что здесь они не прибиты скотчем к первому feature-модулю, а вынесены в отдельные зоны ответственности.

Архитектурный подход

Внутри feature-модулей используется DDD/CQRS-style структура:

domain/
application/
infrastructure/
presentation/

Контроллеры остаются тонкими: они принимают DTO, проходят validation pipe и отправляют command/query в CommandBus или QueryBus. Решения принимаются в handlers, доменные правила живут ближе к entities/domain services, а работа с базой прячется за repository ports.

Например, UsersModule не зависит напрямую от конкретной реализации хранения. Внутри модуля есть абстракции вроде UserRepository, RolePermissionRepository, MagicLinkTokenRepository, а Drizzle-реализации подключаются через providers. Такой же подход используется для связи с auth-service: application layer знает про AuthGatewayPort, а gRPC спрятан в AuthGrpcGatewayAdapter.

Мне нравится этот стиль за простую вещь: когда проект растет, у него уже есть карта. Новый endpoint не обязательно превращается в новый кусок случайного service-кода. Для него есть место: command, query, handler, repository, mapper, DTO.

Почему auth вынесен отдельно

Самая важная эволюция шаблона - отдельный auth-service.

Изначально auth мог жить внутри основного backend, как это обычно бывает в NestJS-проектах. Но password verification через Argon2id - CPU-bound операция. Если логины начинают идти пачкой, основной Node.js процесс может начать хуже отвечать вообще на все endpoint'ы, хотя проблема на самом деле только в auth flow.

Поэтому credential verification, refresh-token rotation, logout, password reset/change и token issuance вынесены в gRPC-сервис. Для клиентов REST API при этом не поменялся: /auth/login, /auth/refresh, /auth/logout по-прежнему живут на основном backend. Внутри HTTP handler вызывает AuthGatewayPort, получает структурированный результат и переводит его обратно в привычные HTTP exceptions, cookies, domain events и i18n messages.

Мне нравится это решение, потому что оно не ломает внешний контракт, но изолирует тяжелую часть. Если login surge создает нагрузку, деградирует auth-service, а не весь продукт.

Security и auth flow

В шаблоне auth сделан не как “выдали JWT и забыли”.

Есть несколько важных деталей:

  • access и refresh tokens подписываются отдельно;
  • refresh tokens хранятся в базе и могут быть отозваны server-side;
  • refresh rotation создает новую пару токенов и отзывает старый refresh token;
  • password change может инвалидировать старые сессии через token version;
  • cookie delivery поддерживает режимы HYBRID, COOKIES_ONLY, RESPONSE_ONLY;
  • CSRF guard реализует double-submit cookie pattern для unsafe methods;
  • brute-force protection ведет окно неудачных попыток, включает captcha после нескольких ошибок и временно блокирует аккаунт после дальнейших попыток;
  • magic link login и password reset вынесены в отдельные flows.

RBAC сделан через CASL. Роли и permissions сидятся при старте: Administrator, Manager, User, Public. Роуты защищаются декларативно через @Policy(action, subject) и guards, а permission matrix хранится как данные, а не как набор случайных if (user.role === ...).

Это не “enterprise ради enterprise”. Это набор вещей, которые почти всегда приходится добавлять позже, если проект проживает достаточно долго.

Файлы, письма, уведомления и captcha

Я отдельно проработал инфраструктурные модули, потому что они обычно становятся источником технического долга.

Файлы хранятся не в локальной папке приложения, а в S3-compatible storage. Локально это MinIO. В базе остаются метаданные и версии файлов, а backend стримит содержимое наружу. Это позволяет не завязывать бизнес-логику на файловую систему контейнера.

Mail module использует Handlebars templates, которые хранятся в PostgreSQL и сидятся при старте. Отправка идет через BullMQ queue, а статус письма сохраняется. Это лучше, чем отправлять email прямо в request handler и надеяться, что SMTP всегда отвечает быстро.

Notifications module устроен похожим образом: in-app уведомления создаются как доменная сущность, а dispatch происходит асинхронно. Модули не дергают друг друга напрямую: например, событие UserCreatedEvent может быть обработано mail-модулем и notification-модулем независимо.

Captcha - отдельная история. Вместо стороннего сервиса сделан свой SVG text captcha. Изображения заранее генерируются batch-задачей, складываются в S3-backed pool и отслеживаются через Redis. Ответы хранятся не в plaintext, а через HMAC hash. Это решение выглядит чуть тяжелее обычной синхронной генерации, но зато captcha challenge не создает лишнюю нагрузку в момент пользовательского запроса.

Bootstrap и production-контур

В main.ts много не декоративной, а полезной plumbing-логики:

  • Fastify adapter;
  • request id propagation через X-Request-Id;
  • CORS allowlist;
  • Helmet/CSP;
  • cookie и multipart plugins;
  • global prefix /api/v1;
  • Swagger с опциональной Basic Auth защитой;
  • global validation pipe;
  • i18n-aware validation exceptions;
  • language/logger/transform interceptors;
  • global exception filter;
  • startup warning, если внешний monitoring не включен.

Docker Compose поднимает полный локальный контур: PostgreSQL, Redis, MinIO, Mailpit, auth-service и backend. У сервисов есть healthchecks, MinIO-init создает bucket и включает versioning, backend зависит от готовности инфраструктуры и auth-service.

Для меня это важная часть шаблона: он должен запускаться как маленькая production-like система, а не только как pnpm start:dev на локальной машине.

Observability и поддержка

Логи идут через Pino. В dev можно получить читаемый pretty output, в production - JSON. Есть optional transports для файла, Graylog/GELF и Elasticsearch. Чувствительные поля редактируются до попадания в лог: Authorization, Cookie, password, accessToken, refreshToken и похожие значения не должны случайно утекать в observability.

Health endpoints разделены на:

  • /health - readiness plus memory thresholds;
  • /health/ready - проверка зависимости от базы;
  • /health/live - процесс жив, без внешних зависимостей.

Плюс есть отдельный docker-compose.zabbix.yaml, который можно подключать как monitoring stack. Это не делает проект “магически production-ready”, но задает правильное направление: сервис должен быть не только написан, но и наблюдаем.

Тестовый контур

В шаблоне есть unit, integration и e2e конфигурации Jest. Unit-тесты лежат рядом с исходниками и мокают abstract repositories/services. Есть e2e harness, который поднимает реальный AppModule и Fastify, плюс health e2e spec.

При этом я не считаю этот участок идеальным. Integration/e2e слой больше подготовлен как каркас, чем заполнен плотным набором сценариев. Это честная зона развития шаблона: инфраструктура для тестов есть, но конкретные проекты должны добавлять свои critical path тесты поверх нее.

Что я считаю главным результатом

Главная ценность проекта не в том, что он “умеет все”. Его ценность в том, что он снимает повторяющуюся боль начала backend-разработки.

Когда я начинаю новый продукт, мне не нужно заново собирать:

  • auth;
  • роли и permissions;
  • config loader;
  • Docker Compose;
  • миграции;
  • seed админа;
  • S3-файлы;
  • очереди;
  • письма;
  • i18n;
  • healthchecks;
  • structured logging;
  • Swagger;
  • request ids;
  • базовый тестовый контур.

Я могу быстрее перейти к домену конкретного проекта: заказам, документам, аукционам, терминалам, marketplace-логике или админским workflow.

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

Комментарии

Пока нет комментариев — будьте первым.