- Project name: honotype-api-nodejs
- Language: TypeScript
- Runtime stack: Node.js, Hono, PostgreSQL, Drizzle ORM, Knex migrations, Zod
- Архитектурный стиль: модульный API server с собственным lightweight framework поверх Hono
- При работе в этом репозитории обязательно соблюдай правила из
CODE_QUALITY.md. CODE_QUALITY.mdявляется источником правды для code quality, naming, style и общих правил написания кода для Codex в этом workspace.- Не дублируй code quality rules в этом файле:
AGENTS.mdописывает архитектуру, слои, flow и проектные ограничения, аCODE_QUALITY.md— правила качества кода.
honotype-api-nodejs не использует NestJS-подобный framework layer с декораторами и DI-контейнером как основу приложения.
Вместо этого проект строит свой небольшой каркас поверх Hono:
- bootstrap и composition root находятся в
apps/api-main/main.tsиapps/api-main/server.ts; - HTTP-модули собираются вручную через массив контроллеров;
- каждый бизнес-модуль живет в собственной папке и имеет явный набор зависимостей;
- вход и выход из системы типизированы через DTO, contracts и resources;
- доступ к данным вынесен в отдельный repository layer;
- framework/runtime primitives и cross-cutting concerns лежат в
libs/core; - общие
inout-артефакты лежат вlibs/common.
Это делает код явным, предсказуемым и удобным для небольшого/среднего backend-проекта без тяжелой магии framework-а.
.old-apiстарая реализация API, не источник правды для новой разработки_templatesканонические шаблоны модулей и файлов. При добавлении новых модулей сначала сверяйся с нимиappsприкладные приложенияapps/api-mainосновное API-приложениеapps/api-main/configконфигурация приложения и env mappingapps/api-main/databaseruntime connection, drizzle schema, knex migrationsapps/api-main/datalayerрепозитории доступа к даннымapps/api-main/globalглобальные зависимости и middlewareapps/api-main/modulesбизнес-модули APIlibs/corekernel/runtime building blocks без доменной логикиlibs/commonобщиеinoutcontracts/resources, которые можно переиспользовать между модулямиlibs/entitiesдоменные сущности
Базовый structural pattern проекта:
apps/*содержит runnable application;libs/coreсодержит kernel/runtime building blocks без доменной логики;libs/commonсодержит общиеinoutcontracts/resources;libs/entitiesсодержит доменные сущности;- alias imports через
tsconfig.jsonиспользуются как обязательная практика.
Это означает, что новый код нужно класть не “по технологическому вкусу”, а по слою ответственности.
Проект использует явный composition root вместо скрытой DI-магии:
apps/api-main/main.tsрешает, запускать сервер или миграции;apps/api-main/server.tsсоздаетHonoapp;- там же подключаются глобальные middleware;
- там же итерируется список контроллеров из
apps/api-main/modules/index.ts; - каждый контроллер создается вручную через
new Controller().
Это важный паттерн проекта: wiring должен быть виден явно в коде.
Контроллеры регистрируются через единый массив:
apps/api-main/modules/index.tsэкспортирует список controller classes;server.tsпроходит по этому списку и монтирует маршруты.
Следствие для проектирования:
- новый HTTP-модуль не “подхватывается автоматически”;
- после создания контроллера его обязательно нужно добавить в
apps/api-main/modules/index.ts.
Ключевой прикладной шаблон проекта:
- controller отвечает только за route binding и чтение входных данных;
- action выполняет use-case;
- controller передает в
BaseController.execute(...)результат action; - action возвращает
ResourceилиResourceList.
Практически это выглядит так:
- controller создает DTO из
await c.req.json(); - controller достает auth-context при необходимости;
- controller создает action и вызывает
run(...); - action работает с repo/service;
- action возвращает resource.
Это основной flow проекта и его нужно сохранять при проектировании новых endpoint-ов.
libs/core/runtime/controller/base.controller.ts реализует шаблон стандартизованного API response:
- action возвращает
Resource/ResourceList; BaseController.execute(...)превращает это в единый JSON-ответ;- здесь же выставляются status, cookies и headers;
- списки автоматически уходят в поле
list, одиночные сущности вdata.
Следствие:
- контроллеры не должны вручную собирать JSON-ответы для обычных бизнес-route-ов;
- ответ должен проходить через
Resource-pipeline, чтобы формат API оставался единым.
BaseAction задает минимальный контракт: run(...args) => Promise<Result>.
Реальный паттерн проекта:
- один action = один use case;
- action получает зависимости через constructor injection;
- action не знает про Hono context;
- action оперирует DTO, entities, repos, services, resources.
Это не CQRS в полном смысле, но action layer уже играет роль use-case/application layer.
Каждый модуль имеет собственную папку dependency/:
interface.tsописывает контракт зависимостей;dependency.tsсоздает concrete dependencies;- actions используют зависимости через этот объект.
Пример:
postsзависит только отPostsRepo;authсобираетUsersRepo,RefreshSessionsRepo,AuthService,jwtService,authConfig.
Это один из главных паттернов проекта: зависимости собираются локально и явно, без скрытого контейнера.
Cross-cutting зависимости выделены отдельно:
apps/api-main/global/dependency/dependency.tsсоздаетjwtServiceиjwtMiddleware;server.tsподключает их глобально;- модуль
authпереиспользуетglobalDeps.jwtService.
То есть в проекте есть два уровня зависимостей:
- global deps для общесистемных concern-ов;
- module deps для конкретного бизнес-модуля.
У каждого полноценного модуля есть папка inout/ с тремя ролями:
validations/— DTO и zod schema;contracts/— shape внешнего ответа;resources/— mapping domain/payload -> contract.
Это один из самых устойчивых шаблонов кодовой базы.
DTO-классы:
- наследуются от
BaseValidator; - валидируют вход прямо в constructor;
- используют
zodкак runtime validation layer; - держат вход типизированным до передачи в action.
Практика проекта:
- schema primitives лежат рядом в
schema.ts; - DTO собирает итоговую Zod-схему через
zodSchema(...); - invalid input приводит к
AppError(ErrorCode.VALIDATION).
Contract описывает публичную форму ответа.
Его задача:
- отделить внутреннюю сущность/модель от внешнего API shape;
- не отдавать клиенту лишние поля из entity/database layer.
Resource инкапсулирует:
- mapping в contract;
- response options: status, cookies, headers, pagination, meta.
ResourceList используется для коллекций и пагинации.
Это означает, что mapping и транспортная упаковка не должны жить внутри action или controller.
Сущности в libs/entities построены как легкие классы поверх BaseEntity:
- constructor делает
Object.assign; - entity может содержать локальное доменное поведение;
- пример:
User.hashPassword,User.checkPassword; - пример:
RefreshSessionгенерируетrefreshTokenпри создании.
Здесь используется не rich domain model во всем проекте, а pragmatic entity pattern:
- данные остаются простыми;
- бизнес-правила добавляются только там, где они действительно нужны.
Data access вынесен в apps/api-main/datalayer.
Общие признаки repo-слоя:
- каждый repo работает с одной сущностью/aggregate;
- repo изолирует SQL/ORM детали от action layer;
- repo возвращает entity instances;
BaseRepoсодержит общую обработку DB ошибок, empty responses и defaults.
Повторяющиеся шаблоны внутри репозиториев:
create,update,findOne,findAll, иногдаfindAndPaginate;- private
buildWhere(...)для сборки фильтров; - маппинг DB row ->
new Entity(result); - перевод DB ошибок в
AppError.
Для новых репозиториев нужно придерживаться этой же структуры.
В проекте есть единая error taxonomy:
AppErrorзадает код, status, description, meta;ErrorCodeфиксирует бизнес- и инфраструктурные коды ошибок;BaseRepoмаппит DB-level errors вAppError;BaseValidatorмаппит validation errors вAppError;- auth/actions бросают
AppErrorдля бизнес-ошибок; globalExceptionHandlerконвертирует всё в единый JSON error response.
Это значит:
- новые слои не должны выбрасывать “сырые” инфраструктурные ошибки наружу без маппинга;
- transport-формат ошибки должен оставаться централизованным.
libs/core/runtime/api/global-handlers/global-exception.handler.ts реализует транспортный шаблон ошибок:
- собирает request context;
- различает
AppError,HTTPExceptionи generic error; - возвращает единый shape
ApiResponseError; - suppress-логирование для части ожидаемых 4xx ошибок.
Паттерн важен для проектирования:
- локальный код не должен дублировать обработку ошибок на route-level;
- handler верхнего уровня является единственным местом формирования error response.
Авторизация построена через два слоя:
JwtMiddlewareразбираетAuthorizationheader и кладет пользователя в context;getCurrentUserJwt(c)валидирует наличие пользователя и бросаетNO_ANONYMOUS_ACCESS.
Это повторяемый guard pattern проекта:
- middleware обогащает request context;
- helper извлекает обязательный auth-context;
- action получает уже проверенный
CurrentUserJwt.
Конфигурация оформлена как явный отдельный слой:
- каждый config-файл объявляет schema env variables;
ValidEnvConfigчитает.envи валидирует значения через Zod;Config<T>возвращает typed config object;appConfig,databaseConfig,authConfigимпортируются как runtime constants.
Это обязательный шаблон для новых конфигов:
- env нельзя читать напрямую из
process.envвнутри бизнес-кода; - новый config должен появляться как отдельный validated module.
В проекте намеренно разделены два сценария работы с БД:
- runtime queries идут через
Drizzle ORM; - schema migrations живут отдельно на
Knex.
Иными словами:
apps/api-main/database/schemas/*— runtime schema для Drizzle;apps/api-main/database/migrations/*— эволюция схемы БД;main.ts --migrationsзапускает отдельный migration flow.
Для последующего проектирования это важное ограничение: изменение данных в runtime и изменение схемы БД проект разделяет разными инструментами.
Папка _templates/module задает канонический шаблон нового модуля:
actions/dependency/inout/contractsinout/resourcesinout/validations<module>.controller.tsindex.ts
Это не пример “на всякий случай”, а фактический blueprint, по которому уже собраны реальные модули posts, users, auth.
В проекте активно используются index.ts как barrel exports:
- на уровне модулей;
- в
inout; - в
dependency; - в
database; - в
libs.
Следствие:
- при добавлении нового файла его обычно нужно подключить в локальный
index.ts; - импортировать лучше через alias/barrel, а не через глубокие относительные пути.
Важно не переобобщать архитектуру:
postsиusersобходятся controller + action + repo;- отдельный service layer есть только там, где есть нетривиальная оркестрация, например в
auth.
Значит, локальный паттерн проекта такой:
- не создавать service “по привычке”;
- добавлять service только когда бизнес-логика реально переиспользуется или требует отдельной инкапсуляции.
Каждый контроллер реализует:
routesgetter;init(): Promise<void>.
Сейчас init() в основном логирует запуск, но сам hook уже является частью шаблона контроллера.
Для новых модулей этот интерфейс нужно сохранять, даже если инициализация пока тривиальна.
- bootstrap entrypoint:
apps/api-main/main.ts - HTTP server composition root:
apps/api-main/server.ts - runtime DB + schemas + migrations:
apps/api-main/database/* - repositories:
apps/api-main/datalayer/* - global middleware/deps:
apps/api-main/global/* - business modules:
apps/api-main/modules/* - reusable runtime/framework layer:
libs/core/* - shared inout resources/contracts:
libs/common/inout/* - domain entities:
libs/entities/*
root— базовый ping endpointauth— регистрация, login, refresh-tokens, Google OAuth flowusers— current user endpointposts— list/create posts
Если проектировать новый модуль в текущем стиле репозитория, ориентируйся на такую последовательность:
- Создать папку
apps/api-main/modules/<module-name>. - Добавить
<module>.controller.ts. - Добавить
actions/с отдельным action на каждый use case. - Добавить
dependency/interface.tsиdependency/dependency.ts. - Добавить
inout/validationsдля DTO и Zod schema. - Добавить
inout/contractsдля публичных response shapes. - Добавить
inout/resourcesдля mapping domain -> contract. - При необходимости добавить
services/, но только если логика действительно сложная. - Если нужны данные, добавить/расширить repo в
apps/api-main/datalayer. - Если меняется схема БД, обновить
database/schemas/*и создать migration. - Экспортировать модуль через локальный
index.ts. - Зарегистрировать controller в
apps/api-main/modules/index.ts.
Для нового endpoint-а проект ожидает такой сценарий:
- Route принимает запрос в controller.
- Controller читает body/query/context.
- DTO валидирует входные данные.
- Controller вызывает соответствующий action.
- Action работает с repo/service/entities.
- Action возвращает
ResourceилиResourceList. BaseController.execute(...)формирует унифицированный HTTP response.
Если endpoint требует авторизацию:
JwtMiddlewareзаранее кладет current user в context.- Controller вызывает
getCurrentUserJwt(c). - Action получает уже готовый typed auth context.
- Предпочитай явную сборку зависимостей неявной магии.
- Держи Hono-specific код только в controller/middleware layer.
- Не пиши бизнес-логику прямо в controller.
- Не отдавай entity/database row напрямую наружу, всегда проходи через contract/resource.
- Валидацию входа держи в DTO через
BaseValidator+zod. - Ошибки маппь в
AppError, а не в произвольныеError. - Repo должен возвращать entity, а не сырые DB rows.
- Новый конфиг оформляй как validated config module.
- Используй alias imports и barrel exports как стандарт проекта.
- Для новых модулей сначала смотри в
_templates/module, потом в ближайший реальный модуль-референс.
- Это lightweight modular server, а не framework-heavy enterprise architecture.
- Главный design goal проекта: явность, низкая магия, небольшой cognitive load.
- Здесь лучше работает простая модульная композиция, чем глубокие уровни абстракции.
- Источник правды для сборки зависимостей — модульные dependency-объекты (
dependency.ts) иglobalDepsдля cross-cutting concerns. - В репозитории на текущий момент не найден сформированный test layer. Для новых фич стоит проектировать тесты отдельно и не рассчитывать на уже устоявшийся тестовый шаблон внутри этого репозитория.
- Источник правды по модульному шаблону:
_templates/moduleи модулиposts,users,auth. - Источник правды по API bootstrap:
apps/api-main/main.tsиapps/api-main/server.ts. - Источник правды по response/error patterns:
libs/core/runtime/api/*иlibs/core/runtime/errors/*. - Источник правды по config pattern:
apps/api-main/config/*,libs/core/kernel/config/*иlibs/core/runtime/config/*. - Источник правды по data access pattern:
apps/api-main/datalayer/*иapps/api-main/database/*.
- Для нового кода сохраняй pattern:
controller -> action -> repo/service -> resource. - Для новых модулей используй
_templates/moduleкак базовый scaffold. - Не читай
process.envнапрямую вне config layer. - Не формируй API response вручную, если можно использовать
ResourceиBaseController.execute(...).