- Added Zod as a dependency in package.json. - Updated pnpm-lock.yaml to include Zod. - Refactored API interfaces: exported new modules for perk, survivor, mission, and encounter. - Removed obsolete api-interfaces.ts file. - Enhanced tests for new schemas in api-interfaces.spec.ts, covering various validation scenarios.
3.7 KiB
Executable File
3.7 KiB
Executable File
0002 — Nx monorepo with enforced module boundaries
- Status: Accepted
- Date: 2026-05-06
Context and problem statement
Fog Expedition has at least three distinct deployable surfaces (Twitch Video Overlay frontend, NestJS EBS backend, content/balance library), and types must be shared between them (DTOs, domain models, encounter records). The project is being built with heavy AI assistance, which adds a specific failure mode: AI assistants happily reach across project boundaries with relative imports if not constrained.
How should the codebase be structured?
Decision drivers
- Type sharing without duplication. The overlay and EBS exchange
Mission,Survivor,EncounterResultetc. Hand-keeping types in sync across separate repos is bug-prone. - AI-assisted-build discipline. AI tools generate plausible-looking code that violates architectural conventions unless those conventions are mechanically enforced.
- Solo-developer simplicity. Multiple repos mean multiple CI configs, multiple versioning streams, multiple onboarding paths.
- Future scaling. Should the project ever grow to more contributors, the architecture should still be coherent.
- Tooling support for the chosen language stack (TypeScript, Angular, NestJS).
Considered options
- Multi-repo (polyrepo). Frontend, backend, and shared types each in their own repository. Shared types published to a private npm registry.
- Loose monorepo (npm/pnpm workspaces only). All projects in one repo, no orchestrator. No automated boundary enforcement.
- Nx monorepo with
enforce-module-boundaries. All projects in one repo. Project tags (scope:api,scope:overlay,scope:shared,type:app,type:lib) define dependency rules enforced at lint time. - Turborepo. Lighter monorepo orchestrator, focused on caching and task running. No native module boundary enforcement.
Decision outcome
Chosen: Nx monorepo with enforced module boundaries.
Project structure:
apps/
api/ # scope:api, type:app
overlay/ # scope:overlay, type:app
libs/
api-interfaces/ # scope:shared, type:lib (Zod schemas, DTOs)
mission-logic/ # scope:shared, type:lib (encounter resolver, perk math)
encounter-library/ # scope:shared, type:lib (content + helpers)
Boundary rules enforced via @nx/enforce-module-boundaries:
- Apps can only depend on libs (not other apps).
scope:apican depend onscope:apiandscope:shared.scope:overlaycan depend onscope:overlayandscope:shared.scope:sharedcan only depend onscope:shared.
Violations fail lint (verified at workspace setup with a deliberate negative test).
Consequences
Positive
- Type sharing is direct via TypeScript imports, no version coordination needed.
- Module boundary enforcement catches architecturally incorrect AI-generated code at lint time before code review.
- Single CI pipeline, single dependency tree, single onboarding flow.
nx affectedruns only what changed, keeping CI fast as the project grows.- Per-project
package.jsonfiles keep workspace libs publish-ready (e.g.,api-interfacescould later be published as a community SDK without restructuring).
Negative
- Steeper learning curve than plain pnpm workspaces — Nx generators, executors, and project configuration are non-trivial.
- Tighter coupling to Nx's release cadence;
nx migrateis required for major version upgrades but generally handles them well. - Some Nx generators lag behind the underlying tooling (e.g.,
@nx/nestdoesn't yet support Vitest natively — see ADR-0003).
Neutral
- The monorepo single source of truth makes architectural mistakes more visible; it also makes them more impactful when they happen.