Правила написания кода
Обязательный документ для агентов и разработчиков, которые пишут код в этом репозитории.
Проектные и архитектурные решения сюда не входят — они в architecture/ и docs/plans/.
Что и как документировать после реализации — в documentation-rules.md.
Соотношение с
repository-guide.md: тот документ описывает, как устроен репозиторий, и читается для понимания. Этот — предписывает, как писать код, и обязателен к исполнению. При расхождении приоритет за правилами.
Правила выведены из фактического кода. Если правило и код расходятся — правило приоритетнее, кроме случаев, перечисленных в разделе «Известные расхождения».
1. Именование
Заголовок раздела «1. Именование»1.1. Файлы и папки
Заголовок раздела «1.1. Файлы и папки»- Всё в kebab-case:
admin-project.service.ts,header-fingerprint.decorator.ts. - Роль файла задаётся суффиксом, а не папкой:
| Суффикс | Что внутри |
|---|---|
*.module.ts | NestJS-модуль |
*-core.module.ts | модуль без контроллеров (только провайдеры на переиспользование) |
*.controller.ts | HTTP-контроллер |
*.gateway.ts | WebSocket-шлюз |
*.service.ts | бизнес-логика |
*.repository.ts | доступ к данным |
*.strategy.ts | Passport-стратегия |
*.guard.ts | guard |
*.decorator.ts | параметр-декоратор |
*.filter.ts | exception filter |
*.helper.ts | чистые функции-хелперы |
*.dto.ts | DTO, лежит в подпапке dto/ (в data-access исторически dtos/) |
*.type.ts / *.interface.ts | типы и интерфейсы |
*.constants.ts | константы модуля |
*.env.ts, *.validation.ts, *.config.ts | части конфиг-модуля |
*.spec.ts | unit-тест, лежит рядом с тестируемым файлом |
- Имя файла = доменный префикс + роль:
admin-project.repository.ts,customer-auth.constants.ts. - Папки внутри библиотеки — во множественном числе по роли:
dto/,guards/,helpers/,constants/,types/,validators/,services/,filters/,decorators/.
1.2. Регистрация библиотеки — четыре имени должны совпадать
Заголовок раздела «1.2. Регистрация библиотеки — четыре имени должны совпадать»Это то место, где чаще всего ломают конвенцию. При создании библиотеки одновременно появляются четыре идентификатора, и все они выводятся из пути, а не придумываются:
| Где | Значение | Пример для libs/apis/providers/user-api/features/profile |
|---|---|---|
project.json → name | путь без libs/apis/, через дефис | user-api-feature-profile |
jest.config.ts → displayName | то же самое, что name | user-api-feature-profile |
tsconfig.base.json → алиас | путь как есть, через слэши | @crewsforge-back/apis/providers/user-api/features/profile |
jest.config.ts → coverageDirectory | coverage/ + полный путь библиотеки | ../../../../../../coverage/libs/apis/providers/user-api/features/profile |
Схемы имён Nx-проектов по типу библиотеки:
| Тип библиотеки | Схема name / displayName | Пример |
|---|---|---|
| фича сервиса | <service>-feature-<feature> | auth-api-feature-customer-auth |
| data-access сервиса | <service>-data-access | project-api-data-access |
| конфиг | config-<scope>-<name> | config-shared-s3, config-auth-api-jwt-customer |
| клиент инфраструктуры | <name>-client | redis-client, mailer-client |
| общая библиотека | api-shared | api-shared |
| утилита | api-util-<name> | api-util-prisma-client |
| доменное ядро | api-core-<name> | api-core-money |
Алиас всегда строится как @crewsforge-back/apis/<то же, что путь после libs/apis/>.
Плоские алиасы вида @crewsforge-back/redis-client — легаси, новые так не заводятся.
Теги в project.json обязательны и состоят ровно из двух:
scope:<service>|shared|utils|config и type:feature|data-access|util|config.
1.3. Классы
Заголовок раздела «1.3. Классы»PascalCase, имя = <Домен><Уточнение><Роль>, где <Роль> дословно повторяет суффикс файла:
admin-project.service.ts → AdminProjectServiceadmin-project.repository.ts → AdminProjectRepositoryadmin-project.controller.ts → AdminProjectControlleradmin-project.module.ts → AdminProjectModulecustomer-core.module.ts → CustomerCoreModuleadmin-access-token.guard.ts → AdminAccessTokenGuardadmin-access-token.strategy.ts→ AdminAccessTokenStrategys3-config.service.ts → S3ConfigServiceagent-chat.gateway.ts → AgentChatGatewayДомен в имени класса не сокращается и повторяется полностью, даже если он уже есть в пути
библиотеки: внутри features/admin-project класс называется AdminProjectService, а не ProjectService.
1.4. DTO
Заголовок раздела «1.4. DTO»<Домен><Действие|Сущность>[Response]Dto. Роль определяется хвостом:
| Хвост | Назначение | Пример |
|---|---|---|
...Dto | тело входящего запроса | AdminLoginDto, BlockUserDto, LeaveRequestDto |
...QueryDto | query-параметры | AdminProjectQueryDto, AgentHistoryQueryDto |
...CreateDto / ...UpdateDto | создание / обновление | CustomerCreateDto, AdminProjectUpdateDto |
...ResponseDto | тело ответа | AdminProjectResponseDto, AdminMeResponseDto |
- Поля DTO — camelCase, обязательные помечаются
!, необязательные —?. - Общие поля
id/createdAt/updatedAtнаследуются отBaseDtoизdata-accessсервиса. - Вложенный DTO объявляется отдельным файлом и подключается через
@Type(() => XxxDto)+@ValidateNested().
1.5. Методы сервисов
Заголовок раздела «1.5. Методы сервисов»Схема: <глагол><One|Many|All>[By<Поле>][OrFail].
| Имя | Смысл |
|---|---|
findAll(query) | список с фильтрами и пагинацией |
findOneById(id) | найти или null |
findOneByIdOrFail(id) | найти или NotFoundException |
findOneByUserIdOrFail(userId) | поиск по другому полю — поле в имени |
updateOneById(id, dto) | обновление |
deleteOneByIdOrFail(id) | удаление |
softDeleteOneById(id, adminId) | мягкое удаление |
saveOne(dto) | создание/сохранение |
Суффикс OrFail означает и обязывает бросать исключение. Метод без OrFail возвращает null
и не бросает. Доменные действия именуются глаголом предметной области: signIn, signUp,
signOut, refreshToken, blockUser, sendVerificationEmailCode.
1.6. Методы репозиториев
Заголовок раздела «1.6. Методы репозиториев»Повторяют словарь Prisma: findMany, findUnique, findFirst, create, update, delete,
deleteMany, count, upsert. Составные операции склеиваются через And:
findManyAndCount, findManyAndPaginate. Для конкретной под-сущности имя дополняется ею:
findUniqueAvatar, updateSkills, deleteLanguages.
1.7. Переменные, поля, зависимости
Заголовок раздела «1.7. Переменные, поля, зависимости»- Локальные переменные, поля, методы, параметры — camelCase.
- Зависимость в конструкторе называется camelCase-версией своего класса и объявляется
private readonly:Единственное закреплённое исключение —constructor(private readonly adminProjectRepository: AdminProjectRepository,private readonly mapperService: MapperService,) {}PrismaClientService, его принято зватьprisma. - Логгер:
private readonly logger = new Logger(XxxService.name)— имя класса берётся через.name, не строкой. - Булевы поля и переменные начинаются с
is/has:isBlocked,isEmailVerified,isFirstPage. - Массивы — во множественном числе (
sourceObjects,variables), одиночные сущности — в единственном. - Сокращения не используются:
adminProjectService, а неapsилиsvc.
1.8. Константы
Заголовок раздела «1.8. Константы»- Модульные константы — UPPER_SNAKE_CASE, живут в
*.constants.ts:DEFAULT_PAGE_SIZE,MAX_PAGE_SIZE,ADMIN_ROUTE_PREFIX. - DI-токены —
Symbolс именем, совпадающим с именем константы:export const REDIS_CLIENT = Symbol('REDIS_CLIENT'); - Ключи Redis и TTL к ним — парой, с суффиксами
_KEYи_TTL:TTL — в секундах, с комментарием в человеческих единицах.export const CUSTOMER_VERIFICATION_CODE_KEY = 'CUSTOMER_VERIFICATION_CODE';export const CUSTOMER_VERIFICATION_CODE_TTL = 900; // 15 min - Имена Passport-стратегий и cookie —
<ROLE>_<ACCESS|REFRESH>_TOKEN_<STRATEGY|COOKIE>_NAMEвlibs/apis/shared/src/lib/constants/token.constants.ts. Строковые значения не хардкодятся в стратегиях и guard’ах — только через эти константы. - Литеральные наборы фиксируются через
as const.
1.9. Маршруты и Swagger
Заголовок раздела «1.9. Маршруты и Swagger»- Пути — kebab-case, ресурсы во множественном числе:
customers/auth/reset-password,employees/me/profiles,projects. - Вложенность отражает владение ресурсом:
customers/me/projects. - Параметры пути — camelCase и осмысленные:
:projectId,:customerId;:idдопустим, когда ресурс в пути один. - Действие над ресурсом — отдельный сегмент-глагол:
:id/block,:id/unblock,:id/restore,:projectId/status. @ApiTagsсовпадает с путём контроллера:@Controller('customers/me/projects')→@ApiTags('customers/me/projects').- Ведущий слэш в путях методов не ставится:
@Get('me'), а не@Get('/me').
1.10. Конфиги и переменные окружения
Заголовок раздела «1.10. Конфиги и переменные окружения»Файлы конфиг-библиотеки <name>:
<name>.env.ts → interface <Name>EnvironmentVariables, get<Name>Environment()<name>-config.validation.ts → export const validationSchema (Joi)<name>.config.ts → export const <name>Config = registerAs('<name>', ...)<name>-config.service.ts → class <Name>ConfigService с геттерами<name>-config.module.ts → class <Name>ConfigModule- Имя namespace в
registerAs— короткое имя конфига в нижнем регистре ('s3','app'), ключи внутри namespace — camelCase (s3.accessKeyId). - Переменные окружения — UPPER_SNAKE_CASE с префиксом домена:
S3_BUCKET,SMTP_HOST,JWT_ADMIN_ACCESS_SECRET_KEY,RABBITMQ_REQUEST_QUEUE. - Геттер сервиса называется по смыслу без префикса домена:
S3ConfigService.bucket, а неs3Bucket.
1.11. Prisma
Заголовок раздела «1.11. Prisma»- Модель — PascalCase в единственном числе:
User,EmployeeProfile,WalriderThread. - Поля — camelCase, физическая колонка —
@map("snake_case"). - Таблица —
@@map("множественное_число_snake_case"):@@map("users"),@@map("employee_profiles"). - Enum — PascalCase, значения — UPPER_SNAKE_CASE:
ProjectStatus.IN_PROGRESS. - Связующие таблицы —
<A>On<B>(UserOnRole) или<Owner><Child>(EmployeeProfileSkill). - Стандартные поля:
id(uuid,@default(dbgenerated("gen_random_uuid()"))),createdAt,updatedAt,deletedAtдля soft delete. - Папка миграции —
<timestamp>_<краткое_описание_snake_case>:20260321000000_add_admin_service.
1.12. Тесты
Заголовок раздела «1.12. Тесты»describe('AdminContactService', () => { // имя тестируемого класса describe('leaveRequest', () => { // имя метода it('sends the lead to the support inbox with reply-to set to the applicant', ...)describe — имя класса, вложенный describe — имя метода, it — утверждение на английском
в третьем лице («что делает»), а не «should …».
1.13. Идентификатор места в логе
Заголовок раздела «1.13. Идентификатор места в логе»action в логе строится как <nx-имя библиотеки>/<Класс>/<метод> — см. раздел 10.3.
1.14. Ветки, коммиты, документы
Заголовок раздела «1.14. Ветки, коммиты, документы»- Ветки:
feat/<kebab-тема>,fix/<kebab-тема>. - Коммиты — Conventional Commits, на английском, в нижнем регистре, со скоупом:
feat(admin-api): add public landing contact endpoints. Типы:feat,fix,docs,refactor,chore,migration. - Документы планов:
docs/plans/YYYY-MM-DD-<kebab-тема>-design.mdиdocs/plans/YYYY-MM-DD-<kebab-тема>.md.
2. Куда класть код
Заголовок раздела «2. Куда класть код»- Бизнес-логика — только в
libs/. Вapps/<service>правим лишьmain.ts(глобальные пайпы, фильтры, Swagger) иapp/app.module.ts(подключение модулей). - Новая фича сервиса →
libs/apis/providers/<service>/features/<feature>/. - DTO, константы, типы, разделяемые несколькими фичами сервиса →
libs/apis/providers/<service>/data-access/. Контроллеров и сервисов вdata-accessбыть не должно. - Guard, декоратор, хелпер, валидатор, нужный больше чем одному сервису →
libs/apis/shared/. - Обёртка над внешней системой →
libs/apis/utils/<name>-client/. - Чтение окружения →
libs/apis/configs/<scope>/<name>/.
3. Слои
Заголовок раздела «3. Слои»Controller → Service → Repository → PrismaClientService / Redis / Rabbit / S3 / Mailer- Controller: маршрутизация, guard’ы, Swagger-декораторы, извлечение данных из запроса, вызов сервиса. Ни бизнес-правил, ни обращений к Prisma.
- Service: бизнес-правила, оркестрация, транзакции, бросание HTTP-исключений.
- Repository: единственное место, где инжектится
PrismaClientService. Возвращает Prisma-сущности, не DTO. - Repository можно не заводить для тривиальной фичи, но зависимости всегда направлены сверху вниз.
*-core.module.tsсоздаётся, когда логику фичи нужно переиспользовать в другом приложении без HTTP-слоя;*.module.tsимпортирует core и добавляет контроллеры.
4. Импорты и границы
Заголовок раздела «4. Импорты и границы»- Между проектами — только алиасы
@crewsforge-back/.... Относительные пути — внутри одного проекта. - Публичный API библиотеки —
src/index.ts. Импортировать внутренние файлы чужой библиотеки нельзя; нужно наружу — добавь реэкспорт вindex.ts. - Границы проверяет
@nx/enforce-module-boundariesпо тегам. Действующие ограничения:type:data-access→ толькоdata-access/scope:shared/scope:utils;type:config→ ни от кого не зависит;scope:admin-api→admin-api,shared,utils,user-api,project-api. - Циклические импорты между библиотеками ломают старт приложения (чинили в
4e3a131). После добавления зависимости проверяйnpx nx graph.
5. DTO, валидация, сериализация
Заголовок раздела «5. DTO, валидация, сериализация»- Каждое поле DTO —
@Expose()+@ApiProperty()+ валидаторыclass-validator. Без@Expose()поле молча пропадает (strategy: 'excludeAll') — это уже ловили в проде (e5bc72c). - Числовые query-параметры — с
@Type(() => Number), строковые — с@Trim()изshared. - Enum-поля объявляются через enum из
@prisma/clientи@IsEnum(...), в@ApiProperty({ enum }). - Сущность → DTO только через
MapperServiceиз@crewsforge-back/apis/shared: он сериализует Prisma-типы (Decimal, Date) передplainToInstance. РучнойplainToInstanceне использовать. - Пагинация —
getPaginate/toPaginateResponseизshared/helpers, ответ в форме{ data, meta }. Размеры страниц — из константDEFAULT_PAGE_SIZE/MAX_PAGE_SIZE. - Пользовательский ввод, попадающий в HTML (письма), экранировать; на это есть тест в
admin-contact.
6. Контроллеры
Заголовок раздела «6. Контроллеры»Обязательный набор декораторов:
@ApiTags('admin-projects')@ApiBearerAuth()@UseGuards(AdminAccessTokenGuard)@Controller('projects')export class AdminProjectController { @ApiOperation({ summary: 'Update a project' }) @ApiResponse({ status: 200 }) @Patch(':id') update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AdminProjectUpdateDto) { return this.adminProjectService.updateOneById(id, dto); }}- Guard’ы и параметр-декораторы (
AdminAccessTokenGuard,@GetTokenPayload(),@Token(),@HeaderFingerprint(),@HeaderSessionId()) берутся изshared, свои не изобретаются. - Публичный эндпоинт без guard’а сопровождается комментарием, почему авторизации нет.
- Идентификаторы в пути — через
ParseUUIDPipe. - Ответ без тела —
@HttpCode(HttpStatus.NO_CONTENT)и возвращаемый типPromise<void>. - Ошибки — штатные исключения Nest (
NotFoundException,BadRequestException,ForbiddenException), самописных классов ошибок нет.
7. Конфигурация
Заголовок раздела «7. Конфигурация»process.envв бизнес-коде запрещён — только типизированныйXxxConfigService. Единственное легальное место чтенияprocess.env— файл*.env.tsконфиг-библиотеки.- Новая переменная окружения требует одновременно: строку в Joi-схеме, геттер в config-сервисе,
заглушку в
jest.setup-env.ts(иначе падают все тесты), строку вdocs/04-shared-and-utils/configs.mdиdocs/06-operations/environment.md, правку соответствующегоdokploy/docker-compose.*.yml. .envв git не коммитится; реальные значения задаются в Dokploy.
8. Данные
Заголовок раздела «8. Данные»- Схема одна:
apps/core-api/prisma/schema.prisma. Команды — всегда с явным--schema:Окно терминала npx prisma generate --schema=./apps/core-api/prisma/schema.prismanpx prisma migrate dev --schema=./apps/core-api/prisma/schema.prisma - Изменение схемы и файл миграции попадают в один коммит. Забытая миграция уже ломала прод (
3734cbc). - Soft delete: в репозиториях фильтровать
deletedAt: null, физически строки не удалять. - Транзакции — декларативно, через
@Transactional()(@nestjs-cls/transactional). - Фильтр по строке —
{ contains: value, mode: 'insensitive' }. include-объекты, повторяющиеся в репозитории, выносятся в модульную константу (const projectInclude = {...}).
9. Стиль
Заголовок раздела «9. Стиль»- Prettier:
singleQuote: true,printWidth: 110, 2 пробела, финальный перевод строки. Перед сдачей —npx prettier --write <изменённые файлы>. - TypeScript strict:
strict,noImplicitOverride,noImplicitReturns,noFallthroughCasesInSwitch,noPropertyAccessFromIndexSignature.anyне добавлять. - Комментарии — редкие и про «почему», а не «что»; язык комментариев русский, как в окружающем коде. Код, идентификаторы, тексты исключений и Swagger-описания — английские.
10. Логирование
Заголовок раздела «10. Логирование»Логирования в коде почти нет: на весь монорепозиторий 31 вызов логгера, из них 17 — строковые, плюс
21 обращение к console.*. Инфраструктура (nestjs-pino, библиотека libs/apis/utils/logger) ещё не
подключена — это Этап 4 плана observability.
Правила ниже — целевая конвенция, и писать по ним нужно уже сейчас: nestjs-pino подменяет логгер
Nest, поэтому обычные new Logger(...) начнут писать JSON без единой правки в местах вызова. Код,
написанный по этим правилам сегодня, при подключении pino заработает как надо; код, написанный строками,
придётся переписывать.
10.1. Форма вызова
Заголовок раздела «10.1. Форма вызова»Лог — всегда объект, никогда не строка:
private readonly logger = new Logger(AdminContactService.name);
this.logger.log({ action: 'admin-contact/AdminContactService/leaveRequest', msg: 'lead accepted', data: { fieldOfActivity },});
this.logger.error({ action: 'admin-contact/AdminContactService/leaveRequest', msg: 'failed to send lead email', err,});10.2. Поля записи
Заголовок раздела «10.2. Поля записи»| Поле | Обяз. | Тип | Назначение |
|---|---|---|---|
action | да | string | где произошло: <библиотека>/<Класс>/<метод> (10.3) |
msg | да | string | что произошло: стабильная фраза без значений (10.4) |
data | нет | object | контекст, только скаляры (10.5) |
err | для warn/error/fatal | Error | объект ошибки отдельным полем (10.6) |
stage | нет | string | этап длинной многошаговой операции |
durationMs | нет | number | длительность операции |
| доменные id | нет | string | userId, projectId, threadId — верхним уровнем, по ним чаще всего ищут |
Добавляются автоматически, руками не писать: time, level, pid, hostname, service (имя сервиса)
и requestId (из ClsModule, сквозная трассировка запроса).
10.3. action — адрес места в коде
Заголовок раздела «10.3. action — адрес места в коде»<nx-имя библиотеки>/<Класс>/<метод>action: 'admin-contact/AdminContactService/leaveRequest'action: 'auth-api-feature-customer-auth/CustomerAuthService/signIn'action: 'rabbit-client/RabbitClientService/connect'- Разделитель — слэш, не дефис. Имена библиотек и так kebab-case (
admin-contact,auth-api-feature-customer-auth), поэтому строка видаadmin-contact-AdminContactService-leaveRequestне разбирается ни глазом, ниsplit('-'). Слэш даёт ровно три однозначных сегмента. - Первый сегмент — Nx-имя библиотеки (раздел 1.2), а не путь и не имя NestJS-модуля.
- Второй и третий — имя класса и метода дословно, как в коде.
- Пишется строковым литералом. Не собирать через
this.constructor.nameи не выносить в переменную: значение должно быть видно в месте вызова и переживать минификацию сборки. - В приватном хелпере указывается сам хелпер, а не вызвавший его публичный метод.
10.4. msg — что произошло
Заголовок раздела «10.4. msg — что произошло»- Английский, нижний регистр, короткая фраза.
- Без интерполяции значений. Строка должна быть стабильной, иначе одинаковые события не группируются в Grafana и по ним нельзя построить счётчик.
- Описывает исход, а не намерение:
lead email sent,login rejected: domain not allowed.
// плохо — значения запечены в текст, фильтровать можно только регуляркойthis.logger.log(`Client ${client.id} connected to project ${projectId}`);
// хорошоthis.logger.log({ action: 'ai-agent-service-feature-agent-chat/AgentChatGateway/handleConnection', msg: 'client connected', projectId, data: { clientId: client.id },});10.5. data — только скаляры
Заголовок раздела «10.5. data — только скаляры»- Допустимы
string,number,boolean,nullи короткие массивы скаляров с ограниченной длиной (напримерallowedDomains). - Запрещено класть Prisma-сущности, DTO, тела запросов и ответов, буферы, массивы объектов. Это раздувает Loki и затягивает в логи то, что туда не собирались класть, включая секреты.
- Вместо коллекции — её размер:
data: { projectsCount: projects.length }. - Идентификаторы, по которым будут искать, выносятся верхним уровнем, а не прячутся в
data; вdataостаётся второстепенный контекст.
10.6. err — ошибка
Заголовок раздела «10.6. err — ошибка»- Передаётся отдельным полем
errи только целым объектом ошибки:errсериализуется вместе со стеком. - Не
err: error.message, не склейка ошибки вmsg. - Только на уровнях
warn,error,fatal.
10.7. Уровни
Заголовок раздела «10.7. Уровни»| Уровень | Когда |
|---|---|
fatal | процесс не может продолжать работу |
error | непредвиденный сбой, требующий вмешательства; всегда с err |
warn | ожидаемый отказ по бизнес-правилу (домен не разрешён, код истёк, пользователь заблокирован) или деградация с ретраем |
info | значимое изменение состояния домена и жизненный цикл сервиса |
debug | шаги внутри операции, нужные при разборе; в проде выключены |
trace | не используем |
this.logger.log(...) в Nest — это уровень info, this.logger.verbose(...) — debug.
Ожидаемая ошибка валидации или авторизации — это warn или вообще ничего, но никогда не error.
Уровень error зарезервирован за тем, на что реально нужно реагировать: иначе алерты по ошибкам
бесполезны.
10.8. Что логировать и чего не логировать
Заголовок раздела «10.8. Что логировать и чего не логировать»Логировать обязательно:
- изменение состояния домена — создание, блокировка, смена статуса, удаление (
info); - отказ по бизнес-правилу или авторизации, непосредственно перед
throw(warn); - перехваченный сбой обращения к внешней системе — БД, S3, SMTP, RabbitMQ, Redis (
errorсerr); - жизненный цикл соединений и приложения:
connected,disconnected,consumer started(info); - начало и конец длинной фоновой операции (
info, на конце —durationMs).
Не логировать:
- успешные чтения и вообще GET-запросы;
- одно и то же событие дважды на разных слоях: логирует тот, кто ошибку обрабатывает, а не тот, кто её пробрасывает;
- внутри цикла по элементам — агрегировать в одну запись со счётчиком;
- то, что и так пишет HTTP-слой: метод, путь, статус, длительность;
- секреты и персональные данные (10.9).
Логирование живёт в сервисах и инфраструктурных клиентах. Контроллеры и репозитории не логируют.
10.9. Секреты и персональные данные
Заголовок раздела «10.9. Секреты и персональные данные»Никогда не попадают в лог: пароли и их хеши, JWT и refresh-токены, коды подтверждения, заголовки
Authorization / Cookie / Set-Cookie, содержимое писем, тела запросов целиком, платёжные данные.
Email в логе — только маскированный (pro***@crewsforge.com); где возможно, идентифицировать
пользователя через userId, а не через email. Логи живут 30 дней и доступны всем, у кого есть вход в
Grafana.
redact на стороне pino — вторая линия обороны, а не разрешение логировать секреты и надеяться на него.
10.10. Технические правила
Заголовок раздела «10.10. Технические правила»- Один вызов логгера = одно событие = одна строка JSON.
console.*в рантайм-коде запрещён. Допустим только вapps/core-api/prisma/seeds/**иapps/*-e2e/src/support/**.- Логгер — поле класса (
private readonly logger = new Logger(XxxService.name)), а не создаётся на каждый вызов. - Тесты не проверяют тексты логов и не завязывают на них assert’ы.
- JSON-поля ≠ лейблы Loki. Лейблы — только
service,level,env.requestId,userId,emailостаются полями: превращение их в лейблы взрывает Loki кардинальностью ровно так же, как Prometheus.
10.11. Приведение существующего кода
Заголовок раздела «10.11. Приведение существующего кода»При правке файла, где уже есть логирование, попутно привести его к конвенции:
- 17 строковых вызовов → структурные (
ai-agent-service,rabbit-client,employee.service.ts); - 13 структурных вызовов в
admin-authиспользуют полеmessageс кодами видаadmin.register.initiatedи логируют сырой email → перевести наaction+msg, email убрать или маскировать; console.*в рантайм-библиотеках (mailer-client,redis-client,email-sender,admin-contact) → логгер.
Отдельной задачей это не гонится — но новый код пишется сразу правильно.
11. Проверки перед сдачей работы
Заголовок раздела «11. Проверки перед сдачей работы»npx prettier --write <файлы>npx nx lint <project>npx nx test <project>npx nx build <app># либо разом:npx nx affected -t lint,test,buildCI в репозитории нет — проверки только локальные, поэтому пропускать их нельзя. Заявлять «готово» можно после реального запуска команд и просмотра вывода, а не по факту правки файлов. Коммитить и пушить — только по явной просьбе.
12. Чек-лист «новая фича»
Заголовок раздела «12. Чек-лист «новая фича»»- DTO в
data-accessсервиса (@Expose()+@ApiProperty()+ валидаторы). - Папка
libs/apis/providers/<service>/features/<feature>/сmodule/controller/service/ (repository), плюсproject.json,tsconfig.json,tsconfig.lib.json,tsconfig.spec.json,jest.config.ts,README.md. - Все четыре имени библиотеки согласованы (раздел 1.2), теги проставлены.
- Публичный API — через
src/index.ts. - Алиас в
tsconfig.base.jsonпо схеме@crewsforge-back/apis/.... - Модуль подключён в
apps/<service>/src/app/app.module.ts. - Guard’ы и маппер — из
shared, конфиги — изconfigs, клиенты — изutils. - Миграция Prisma (если менялась схема) и полный обвяз новой env-переменной (раздел 7).
- Логирование по разделу 10: структурные вызовы,
action+msg, без секретов иconsole.*. - Тесты
*.spec.ts; lint / test / build зелёные. - Документация обновлена по
documentation-rules.md:README.mdбиблиотеки, при необходимости еёdocs/, и страница сервиса вdocs/03-services/.
13. Известные расхождения — не копировать
Заголовок раздела «13. Известные расхождения — не копировать»- Имена библиотек. Новые
admin-*библиотеки зарегистрированы какadmin-contact,admin-project,admin-user,admin-auth,jwt-adminи особенноdata-access— в обход схемы из раздела 1.2. Правильные имена были быadmin-api-feature-contact,admin-api-data-access,config-auth-api-jwt-admin. Ориентироваться на схему, а не на эти имена. - Теги. Проставлены лишь у пяти библиотек из тридцати шести, поэтому правила границ Nx сейчас работают частично. У новых библиотек теги обязательны.
- Алиасы. В
tsconfig.base.jsonсоседствуют две схемы; каноническая —@crewsforge-back/apis/.... ValidationPipe. Настроен по-разному в разных сервисах; эталон —apps/admin-api/src/main.ts(whitelist,forbidNonWhitelisted,transform).@Controller. Часть контроллеров задаёт версию явно ({ path, version: '1' }), часть — нет. Для новых кода достаточно строкового пути: версионирование включено глобально.- Папка DTO. В старых
data-accessвстречаетсяdtos/, в новом коде —dto/.