Skip to content

Як ми це будуємо

Для контриб'юторів: як влаштований код усередині 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). Це суворо.