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