Files
fog/docs/adr/0002-nx-monorepo-with-strict-boundaries.md
Maurycy 65af268b86 Add Zod dependency and update API interfaces
- 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.
2026-05-07 00:46:03 +00:00

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, EncounterResult etc. 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

  1. Multi-repo (polyrepo). Frontend, backend, and shared types each in their own repository. Shared types published to a private npm registry.
  2. Loose monorepo (npm/pnpm workspaces only). All projects in one repo, no orchestrator. No automated boundary enforcement.
  3. 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.
  4. 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:api can depend on scope:api and scope:shared.
  • scope:overlay can depend on scope:overlay and scope:shared.
  • scope:shared can only depend on scope: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 affected runs only what changed, keeping CI fast as the project grows.
  • Per-project package.json files keep workspace libs publish-ready (e.g., api-interfaces could 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 migrate is required for major version upgrades but generally handles them well.
  • Some Nx generators lag behind the underlying tooling (e.g., @nx/nest doesn'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.