Files
apf_portal/docs/setup/03-angular-nx-monorepo.md
T
Julien Gautier 0e58e32d29 chore: relocate ADRs from decisions/ to docs/decisions/ to consolidate documentation
Move the ADR folder under docs/ alongside the rest of the project
documentation. Convention (flat folder, globally-sequential 4-digit
numbering, tags-based categorization, MADR 4.0.0 format) is unchanged
- only the path moved.

- git mv decisions docs/decisions preserves history for all 18 ADRs +
  README + template (19 files renamed in this commit).
- ADR-0001 amended in-place with a dated note documenting the
  relocation. Status remains 'accepted' - the location detail
  changed, the decision did not.
- All cross-references updated:
  - CLAUDE.md (~17 ADR links + 3 mentions of decisions/ in the Project
    rules section)
  - docs/README.md (now references decisions/ as a sibling under docs/)
  - docs/setup/03-angular-nx-monorepo.md (paths shortened from
    ../../decisions/ to ../decisions/, since setup/ and decisions/ are
    now both inside docs/)
  - docs/decisions/0003 ../CLAUDE.md adjusted to ../../CLAUDE.md
    (one extra level of nesting)
  - docs/decisions/template.md mention of the README path
  - notes/asvs-level-decision-briefing-rssi.md mention of the index

Sanity verified: every ADR link in CLAUDE.md, docs/setup/03, and
docs/decisions/0001 resolves to an existing file. pnpm nx run-many
-t lint passes on 8 projects.
2026-04-30 18:57:59 +02:00

10 KiB

🏗️ Bootstrap the Nx monorepo for apf-portal

Status. Aligned with phase-1 ADRs (0002, 0003, 0004, 0005, 0006). Replaces the original placeholder-based draft.

Goal

Scaffold the apf-portal Nx monorepo containing:

  • apps/portal-shell — Angular SPA frontend (CSR, standalone, zoneless, Signals, Vitest, SCSS, strict TS).
  • apps/portal-bff — NestJS BFF backend (Express adapter, global ValidationPipe, Jest test runner).
  • libs/feature/<name> and libs/shared/<scope> — feature and shared libraries.

Out of scope here (covered by later phases): authentication (Entra ID), sessions (Redis), observability (Pino + OpenTelemetry), audit trail, accessibility / performance / security baselines in CI, downstream API integration, on-prem infrastructure stack.


Prerequisites

  • WSL + Zsh — see 01-wsl-terminal-setup.md.
  • Node.js latest LTS + pnpm via corepack — see 02-dev-web-stack.md.
  • Working tree under the WSL filesystem (~/dev/, ~/Works/), never /mnt/c/.
  • The repository apf-portal already exists locally with the ADRs and docs committed (you are reading one of them).

1) Initialize the Nx workspace inside the existing repository

The repository already carries documentation, ADRs, .vscode/, and .gitignore. create-nx-workspace always generates into a new directory, so we generate next to the repo and copy the workspace files in.

# 1. Generate a clean Nx workspace next to the repo
cd ~/dev   # or any scratch location outside the repo
pnpm dlx create-nx-workspace@latest apf-portal-bootstrap \
  --preset=apps \
  --pm=pnpm \
  --ci=skip \
  --useGitHub=false

# 2. Copy the workspace files into the existing repo
cd apf-portal-bootstrap
cp -v package.json pnpm-workspace.yaml nx.json tsconfig.base.json \
       .prettierrc .prettierignore .editorconfig \
       eslint.config.mjs \
       ~/Works/apf_portal/

# 3. Drop the scratch workspace
cd ~ && rm -rf ~/dev/apf-portal-bootstrap

The apps preset is generic — it does not privilege a runtime (ADR-0002). Plugins are added in step 2.

Verify:

cd ~/Works/apf_portal
pnpm install
ls nx.json package.json tsconfig.base.json

2) Install Angular and Node plugins

pnpm add -D @nx/angular @nx/nest @nx/vite @nx/eslint @nx/js

3) Generate the frontend app — portal-shell

pnpm nx g @nx/angular:application portal-shell \
  --directory=apps/portal-shell \
  --routing=true \
  --standalone=true \
  --style=scss \
  --bundler=esbuild \
  --unitTestRunner=vitest \
  --e2eTestRunner=playwright \
  --strict=true \
  --tags="scope:portal-shell,type:app"

Enable zoneless change detection (ADR-0004)

Edit apps/portal-shell/src/app/app.config.ts:

import { ApplicationConfig, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [provideZonelessChangeDetection(), provideRouter(routes)],
};

Remove every reference to zone.js:

  • delete any import 'zone.js' from apps/portal-shell/src/main.ts and apps/portal-shell/src/test-setup.ts;
  • drop zone.js from any polyfills array in apps/portal-shell/project.json;
  • check the lockfile no longer pulls zone.js.

Verify:

grep -RIn "zone.js" apps/portal-shell || echo "no zone.js references — OK"

Strict TypeScript (workspace-wide)

In tsconfig.base.json:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noPropertyAccessFromIndexSignature": true,
  },
}

4) Generate the backend app — portal-bff

pnpm nx g @nx/nest:application portal-bff \
  --directory=apps/portal-bff \
  --unitTestRunner=jest \
  --tags="scope:portal-bff,type:app"

Test runner note. NestJS ships with Jest. Vitest unification across the monorepo is possible (via @nx/vite:configuration) and remains a future option — no ADR yet.

Wire the global ValidationPipe (ADR-0005)

Edit apps/portal-bff/src/main.ts:

import { NestFactory } from '@nestjs/core';
import { ValidationPipe } from '@nestjs/common';
import { AppModule } from './app/app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    }),
  );

  // Phase-2 security ADRs will add: helmet, CORS allowlist, cookie-session,
  // CSRF protection, rate limiting, auth guards, structured error filter.

  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

5) Add PostgreSQL + Prisma (ADR-0006)

pnpm add -D prisma
pnpm add @prisma/client nestjs-prisma

mkdir -p apps/portal-bff/prisma
pnpm exec prisma init \
  --datasource-provider postgresql \
  --schema apps/portal-bff/prisma/schema.prisma

Wire PrismaModule in apps/portal-bff/src/app/app.module.ts:

import { Module } from '@nestjs/common';
import { PrismaModule } from 'nestjs-prisma';

@Module({
  imports: [PrismaModule.forRoot({ isGlobal: true })],
})
export class AppModule {}

Commit a .env.example (the actual .env is git-ignored):

DATABASE_URL="postgresql://portal:portal@localhost:5432/portal_dev?schema=public"

Out of scope. PostgreSQL provisioning (HA, backups, RLS policies for the dual-audience model) and Prisma migration deployment will be handled by infrastructure ADRs (phase 3).


6) Generate the first libraries

Convention (ADR-0003):

  • libs/feature/<name> for vertical features;
  • libs/shared/<scope> for cross-cutting concerns.

Examples:

# Frontend feature lib
pnpm nx g @nx/angular:library feature-auth \
  --directory=libs/feature/auth \
  --standalone=true \
  --tags="scope:portal-shell,type:feature"

# Shared UI lib (frontend-only)
pnpm nx g @nx/angular:library shared-ui \
  --directory=libs/shared/ui \
  --standalone=true \
  --tags="scope:portal-shell,type:shared"

# Shared utility lib (consumed by both apps)
pnpm nx g @nx/js:library shared-util \
  --directory=libs/shared/util \
  --tags="scope:shared,type:shared"

7) Enforce module boundaries

In eslint.config.mjs, configure @nx/enforce-module-boundaries with the project tags above:

{
  '@nx/enforce-module-boundaries': [
    'error',
    {
      depConstraints: [
        { sourceTag: 'scope:portal-shell', onlyDependOnLibsWithTags: ['scope:portal-shell', 'scope:shared'] },
        { sourceTag: 'scope:portal-bff',   onlyDependOnLibsWithTags: ['scope:portal-bff',   'scope:shared'] },
        { sourceTag: 'type:app',     onlyDependOnLibsWithTags: ['type:feature', 'type:shared'] },
        { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:shared'] },
        { sourceTag: 'type:shared',  onlyDependOnLibsWithTags: ['type:shared'] },
      ],
    },
  ],
}

This prevents portal-shell from importing portal-bff code (and vice versa), and keeps shared-* libraries free of feature-level dependencies.


8) Prettier, git hooks, and Conventional Commits

See ADR-0007 for the rationale (Husky + lint-staged + commitlint with Conventional Commits).

.prettierrc (project-wide):

{
  "singleQuote": true,
  "semi": true,
  "printWidth": 100
}

Install Husky, lint-staged, commitlint:

pnpm add -D husky lint-staged @commitlint/cli @commitlint/config-conventional
pnpm exec husky init

.husky/pre-commit:

pnpm exec lint-staged

.husky/commit-msg:

pnpm exec commitlint --edit "$1"

commitlint.config.cjs (workspace root):

module.exports = { extends: ['@commitlint/config-conventional'] };

package.json excerpt:

{
  "scripts": { "prepare": "husky" },
  "lint-staged": {
    "*.{ts,html,scss,md,json}": ["pnpm nx format:write --uncommitted"]
  }
}

9) CI/CD

The pipeline targets the Gitea repository (gitea@git.unespace.com:julien/apf_portal.git). Pipeline shape (Gitea Actions vs. third-party runner) and branch protection are deferred to phase-3 ADRs (planned: ADR-0017). Locally, the equivalent of the future CI check is:

pnpm nx affected -t lint test build

10) Daily workflow

# Dev servers
pnpm nx serve portal-shell        # http://localhost:4200
pnpm nx serve portal-bff          # http://localhost:3000

# Tests
pnpm nx test portal-shell         # Vitest
pnpm nx test portal-bff           # Jest

# Quality sweeps
pnpm nx run-many -t lint test     # full
pnpm nx affected -t lint test build   # only what changed since main

# Generators
pnpm nx g @nx/angular:component <name> --project=portal-shell --standalone
pnpm nx g @nx/nest:resource <name> --project=portal-bff

What this guide does not cover (yet)

Concern Phase
Authentication (Entra ID workforce + External ID) 2
Sessions / cache (Redis self-hosted) 2
Downstream API access (existing apps) 2
Observability (Pino + OpenTelemetry + W3C) 2
Audit trail 2
Accessibility baseline (WCAG 2.2 AA + axe-core in CI) 3
Performance budgets (Lighthouse CI) 3
Security baseline (CSP, dep / secret scanning, OWASP ASVS) 3
CI/CD on Gitea 3
On-prem infrastructure stack (orchestration, Postgres HA, Redis HA, observability backend) 3