NestJS задаёт структуру, но не спасает от «god service» и inconsistent API. Ниже — практики, которые масштабируются от MVP до десятков модулей.

Границы модулей
Один bounded context = один Nest module (shop, auth, courses). Shared — только utils без бизнес-логики.
- Controller — HTTP, Service — use cases, Repository/Model — persistence
- Cross-module — через публичный service или events, не import private files
DTO и валидация
- class-validator на все входящие body/query
- Единый формат ошибок { statusCode, message, errors[] }
- Whitelist + forbidNonWhitelisted против mass assignment
Auth и multi-tenant
- JwtAuthGuard + RolesGuard + OptionalJwt для guest checkout
- tenantId из JWT, не из body
- Admin routes под отдельным prefix /admin

Пагинация и фильтры
Cursor pagination для лент; offset — только для админок с малым объёмом.
Чеклист
- OpenAPI/Swagger актуален
- Idempotency-Key на payment endpoints
- Health + readiness probes
- Versioning /api/v1 при breaking changes
Структура ошибок API
Клиенту всегда возвращайте предсказуемый JSON: код, человекочитаемое сообщение, массив field errors для форм.
Внутренние stack trace не утекают наружу; correlation id (traceId) пишется в лог и отдаётся клиенту для support.
Версионирование без боли
Breaking changes — только через /v2 или заголовок Accept-Version. Старый контракт живёт минимум один релизный цикл с deprecation warning в OpenAPI.
Тестирование контракта
E2e на critical paths (auth, checkout, webhook) + contract tests для mobile/BFF. При изменении DTO — обновляйте Swagger и consumer tests в одном PR, иначе фронт и партнёры ломаются «тихо».
Interceptor и logging
Global interceptor добавляет requestId, duration, userId в лог. Exception filter мапит domain errors (InsufficientFunds) в 402/409, не в generic 500.
Для admin endpoints — отдельный rate limit и audit log: кто менял site_settings, кто выдал refund. Это дешевле, чем расследование без trail.
Как разобраться с «god service»
- Нарисуйте граф import между модулями — циклы видны сразу
- Вынесите shared logic в domain service с явным public API
- Один модуль за спринт: controller остаётся, logic уезжает
- Добавьте e2e на один critical flow после каждого split