Як ми це будуємо
Для контриб'юторів: як влаштований код усередині api. Весь бекенд слідує CleanSlice (NestJS + Prisma) у варіанті agentfy — передбачувана структура, де будь-яка фіча виглядає як будь-яка інша.
Коротко: код розбитий на слайси (по одній самодостатній фічі), слайси зібрані в шари (групи), і залежності завжди йдуть лише вниз. Уперше тут? Глосарій пояснює кожен термін нижче.
Анатомія слайсу
slices/<group>/<slice>/
├── <slice>.module.ts # NestJS-модуль
├── <slice>.controller.ts # @Controller('plural') — REST
├── <slice>.prisma # Prisma-модель у корені слайсу (якщо є дані)
├── <slice>.tool.ts # опц.: експозиція через MCP (@Tool)
├── domain/ # ЩО він робить (контракти + логіка)
│ ├── index.ts
│ ├── <slice>.types.ts # IXxxData, ICreateXxxData, XxxTypes (enum)
│ ├── <slice>.gateway.ts # абстрактний IXxxGateway ← DI-токен
│ ├── <slice>.service.ts
│ └── errors/ # доменні помилки (extends BaseError)
├── data/ # ЯК (реалізація)
│ ├── <slice>.gateway.ts # конкретна реалізація (через PrismaService)
│ └── <slice>.mapper.ts # трансформації (sync, без промісів)
└── dtos/
├── <slice>.dto.ts · create<Slice>.dto.ts · update<Slice>.dto.ts · filter<Slice>.dto.tsПотік: Controller → IXxxGateway (абстрактний) → XxxGateway (реалізація) → Mapper → Prisma.
Правила
- Gateway-патерн — абстрактний
IXxxGatewayуdomain/, конкретний уdata/. Prisma і є репозиторій — жодних класів*Repository. - Singular-імена слайсів; camelCase для DTO-файлів;
I-префікс для інтерфейсів; суфіксTypesдля enum;#alias-імпорти; теги-заголовки@scope/@slice/@layer/@type. - Prisma-моделі лежать у корені слайсу (
<slice>.prisma), збираються вinfra/prisma. Генерувати треба командоюbun run prisma:generate, яка спершу збирає: голийnpx prisma generateвиходить нулем на застарілій збірці й віддає клієнт, який не знає вашої нової моделі. - Кожна відповідь загорнута в конверт. Контролер повертає сирі доменні дані; перехоплювач відповіді загортає їх у
{ success, data }. У Swagger це оголошується декоратором конверта, а не вписуванням обгортки в DTO — інакше вийде два описи однієї форми. - Інваріант живе на шляху запису, а не в DTO. DTO — це край: він дає швидку відмову на поганий HTTP-запит і ніколи не бачить ні імпортера, ні фонової задачі, ні тула. Правило, що живе лише там, не забезпечене — воно тільки оголошене.
- Константа оголошується один раз і імпортується. Межа, вписана в DTO, сервіс і форму, розійдеться при першій же правці; за цей урок тут уже платили не раз.
- Ресурс чужої команди відповідає not-found, а не forbidden. 403 повідомляє викликачу, що id існує; між орендарями це саме по собі витік. Id команди, членом якої викликач не є, невідрізненний від id, якого ніколи не було.
- Контролер не має права дотягуватися до шару даних. Ні
*Gateway, ні імпорту зdata/: контролер залежить від сервісу, а сервіс — від інтерфейсу гейтвея. Це не порада — перевірка меж таке відхиляє. - Ідентифікатор команди видає аутентифікація і більше ніщо. Тип, який приймає орендарний гейтвей, не можна зібрати зі звичайного рядка, тому «взяти команду з тіла запиту» не компілюється. Це захист від поспіху, а не від злого наміру — приведення типів його обходить, — і поспіх якраз і трапляється на практиці.
- Спеки лежать у теці
testsпоруч із кодом: у корені слайсу, вdomain/і вdata/, і лише на цих трьох рівнях. Ко-локація лишається правилом; змінилося те, що в корені слайсу більше не лежить чотирнадцять файлів спеків між модулем і контролером. Jest знаходить їх на будь-якій глибині.
Інваріант шаровості
Найважливіше структурне правило:
Залежності між групами йдуть лише вниз. Верхній шар може залежати від нижніх; нижній шар ніколи не імпортує верхній.
Розміщуючи слайс, перевір, що він залежить лише вниз. Якщо з'являється ребро вгору — інвертуй його (порт/інтерфейс, подія або читання через Prisma), а не ламай шар. Див. Шари-слайси.
Що насправді перевіряє гейт
make check — це лінт, перевірка меж і збірка, по всіх чотирьох застосунках: api, app, admin і worker. Дві речі варто знати раніше, ніж вирішити, що ви щось зламали:
- Він звіряє встановлене з лок-файлом. Залежність, яку хтось додав учора, а ви не встановлювали, раніше була тут невидима і спливала через добу як відмова старту в чиємусь терміналі. Тепер вона названа. Лайка
depsна правку, що не чіпаєpackage.json, — це ваше середовище, а не ваш диф: зробіть встановлення. - Спеки виключено і з перевірки меж, і зі збірки — за іменем файлу. Тому зелений гейт нічого не каже про те, чи компілюються спеки; проганяйте
tscокремо, коли правка може їх зачепити — наприклад, при переписуванні шляхів.
Сама перевірка меж бере порядок груп з cleanslice.json кожного застосунку, а не зі скрипта, — тому один скрипт обслуговує і вісім груп api, і інші набори app та admin.
MCP-first (правило проєкту)
Перед написанням чи зміною коду звіритися з CleanSlice MCP (get-started, search з 2+ запитами, read-doc). Це суворо.