Rules for coding agents working in nexus-forge.
- Runtime/framework: NestJS 11 (
apps/api) - Language/tooling: TypeScript (
strict: true), ESLint, Jest - Data: PostgreSQL + TypeORM
- Async: BullMQ (Redis)
- Messaging: NATS (
@nestjs/microservices) with outbox + DLQ pattern - Auth: JWT + Passport + RBAC guard/decorator
- API docs: Swagger at
/api/docs - App prefix + validation: global
/apiprefix and strictValidationPipe
Use feature-first modules under apps/api/src/modules/*.
Inside each module, follow this dependency direction:
interface(controllers/dto) ->application(commands/queries/services) ->domain(aggregate/entities/rules/ports)infrastructureimplements domain/application ports (TypeORM repos, external clients, queue adapters)
Practical mapping in this repo:
- Existing
projectsmodule already containsdomain,commands,queries,infra,outbox. - For new modules, prefer explicit
application/+interface/folders, but preserve existing local style if refactoring is out of scope.
Non-negotiables:
- No business rules in controllers.
- No NestJS/TypeORM dependencies inside domain objects.
- No cross-module deep imports into another module's
infra. - Share only stable contracts/types via
libs/contractsand generic helpers vialibs/shared.
- Depend on tokens/interfaces from domain/application layers (e.g.,
PROJECT_REPOSITORYpattern). - Register concrete adapters in module providers.
- Avoid
@Global()modules except true cross-cutting infrastructure (currentMessagingModuleis an accepted exception).
- Use
ConfigService; avoid rawprocess.envoutside bootstrap/infrastructure wiring. - Validate env on startup (introduce schema validation when touching config).
- Keep production-safe defaults conservative; never default secrets to unsafe values beyond local dev.
- Default for non-trivial business flows in this repo.
- Simple CRUD may stay simple if CQRS adds no clarity.
- One command = one business intent and one handler.
- Command handlers orchestrate use cases; domain enforces invariants.
- Commands should return minimal write results (
id, status), not read-model payloads. - Write flows that persist state + emit events must be atomic (single transaction boundary).
- Must be side-effect free.
- Query handlers may use read-optimized queries/projections.
- Return response DTOs, not ORM entities.
- Emit domain events for meaningful state transitions.
- Publish integration events through outbox after commit, not inline before transaction success.
- Keep event names and payloads versionable and backward compatible.
When changing event publication behavior:
- Preserve
FOR UPDATE SKIP LOCKEDstyle safe concurrent polling. - Keep retry limits and dead-letter transition behavior intact.
- Ensure replay paths are idempotent and auditable.
- Avoid long-running external calls inside open DB transactions unless explicitly justified.
- Controllers are thin adapters: validate/map request -> dispatch command/query -> map response.
- Use
class-validatorDTOs for all external inputs. - Keep global validation guarantees (
whitelist,forbidNonWhitelisted,transform). - Keep Swagger decorators/docs updated for new endpoints.
- Normalize error mapping (domain/app errors -> HTTP exceptions) consistently.
- Repositories are ports in domain/application, adapters in
infra. - Do not leak TypeORM entities outside infrastructure boundaries.
- Use explicit migrations for schema evolution; do not rely on
synchronizefor production rollout. - Enforce critical invariants at both domain and database levels (constraints/indexes).
- Use NATS for integration events; keep subject naming consistent and domain-oriented.
- Use BullMQ for background work, retries, and delayed processing.
- Handlers/consumers must be idempotent (at-least-once semantics).
- Include retry/backoff and poison-message handling strategy for new consumers.
- Protect privileged endpoints with JWT auth + RBAC roles.
- Do not log secrets, tokens, raw credentials, or sensitive payload fields.
- Use least-privilege credentials for DB/Redis/NATS.
- Test aggregates/domain services for invariants and edge cases.
- Test command/query handlers with mocked ports.
- Test TypeORM repositories, outbox repository, and messaging adapters.
- Cover transactional behavior where command + outbox writes must be atomic.
- Cover critical API flows via
apps/api/test. - Include validation failures, auth/RBAC, and happy-path CQRS flows.
Minimum expectation for behavior changes:
- Add or update tests at the closest meaningful level.
- Do not merge significant behavior changes with zero automated coverage.
- Use structured logs with context (module, command/query, aggregate id/event id).
- Add metrics/tracing for command latency, query latency, outbox backlog, publish failures, DLQ size.
- Add health checks for Postgres, Redis, and NATS when touching ops surfaces.
- Keep public method return types explicit.
- Prefer small, cohesive files; split large handlers/services.
- Preserve backward compatibility for external APIs/events unless versioned change is intentional.
- Required local verification for meaningful changes:
npm run lintnpm testnpm run test:e2e(when endpoint or module wiring changes)
Use this scaffold for new non-trivial modules under apps/api/src/modules/<feature>.
apps/api/src/modules/<feature>/
<feature>.module.ts
interface/
<feature>.controller.ts
dto/
create-<feature>.dto.ts
<feature>-response.dto.ts
application/
commands/
create-<feature>.command.ts
create-<feature>.handler.ts
queries/
get-<feature>-by-id.query.ts
get-<feature>-by-id.handler.ts
<feature>-write.service.ts
domain/
<feature>.aggregate.ts
<feature>.repository.ts
events/
<feature>-created.event.ts
<feature>-created.handler.ts
infrastructure/
persistence/
<feature>.entity.ts
typeorm-<feature>.repository.ts
messaging/
outbox.repository.ts
outbox.publisher.ts
Template notes:
- Keep controller methods as transport adapters only.
- Put invariants in aggregate/domain services, not handlers.
- Use repository token pattern in
domain/<feature>.repository.tsand bind in<feature>.module.ts. - Use transaction boundary in write service for
state change + outbox enqueue. - For very small modules,
application/andinterface/can be flattened temporarily, but preserve dependency direction.
Use this only when logic is straightforward (basic CRUD, no complex invariants/workflows, no event choreography required).
apps/api/src/modules/<feature>/
<feature>.module.ts
<feature>.controller.ts
<feature>.service.ts
dto/
create-<feature>.dto.ts
update-<feature>.dto.ts
<feature>-response.dto.ts
infra/
<feature>.entity.ts
typeorm-<feature>.repository.ts
Decision rules:
- Choose simple CRUD template when:
- Writes are direct and low-risk.
- Read model is nearly identical to write model.
- No async/event-driven integration is needed.
- Choose CQRS template when:
- Write invariants or workflows are growing.
- Read and write models diverge.
- You need outbox/integration events, retries, or background orchestration.
Simple CRUD guardrails:
- Controllers remain thin; service contains application logic.
- Keep DTO validation and explicit response mapping.
- Do not leak TypeORM entities directly from controllers.
- If complexity increases, migrate incrementally toward CQRS instead of piling logic into one service.
- Identify affected module boundary and use case.
- Define/adjust command-query contracts and domain rules.
- Implement application logic and infrastructure adapters.
- Wire module providers/controllers.
- Add/update tests.
- Run validation commands.
- Summarize tradeoffs and any remaining risks.
- Fat controllers or handlers containing domain logic and persistence details together.
- Returning TypeORM entities directly from controllers.
- Publishing integration events before durable commit.
- Adding cross-module imports into another module's infrastructure internals.
- Introducing CQRS ceremony for trivial endpoints without clear benefit.