# 🏗️ Bootstrap the Nx monorepo for apf-portal > **Status.** Aligned with phase-1 ADRs ([0002](../decisions/0002-adopt-nx-monorepo-apps-preset.md), [0003](../decisions/0003-workspace-and-app-naming-convention.md), [0004](../decisions/0004-frontend-stack-angular-csr-zoneless-signals.md), [0005](../decisions/0005-backend-stack-nestjs.md), [0006](../decisions/0006-persistence-postgresql-prisma.md)). 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/` and `libs/shared/` — 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](01-wsl-terminal-setup.md). - Node.js latest LTS + pnpm via corepack — see [02-dev-web-stack.md](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. ```bash # 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](../decisions/0002-adopt-nx-monorepo-apps-preset.md)). Plugins are added in step 2. Verify: ```bash cd ~/Works/apf_portal pnpm install ls nx.json package.json tsconfig.base.json ``` --- ## 2) Install Angular and Node plugins ```bash pnpm add -D @nx/angular @nx/nest @nx/vite @nx/eslint @nx/js ``` --- ## 3) Generate the frontend app — `portal-shell` ```bash 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`: ```typescript 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: ```bash grep -RIn "zone.js" apps/portal-shell || echo "no zone.js references — OK" ``` ### Strict TypeScript (workspace-wide) In `tsconfig.base.json`: ```jsonc { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, }, } ``` --- ## 4) Generate the backend app — `portal-bff` ```bash 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`: ```typescript 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) ```bash 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`: ```typescript 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): ```dotenv 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](../decisions/0003-workspace-and-app-naming-convention.md)): - `libs/feature/` for vertical features; - `libs/shared/` for cross-cutting concerns. Examples: ```bash # 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: ```js { '@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](../decisions/0007-pre-commit-hooks-and-conventional-commits.md) for the rationale (Husky + lint-staged + commitlint with Conventional Commits). `.prettierrc` (project-wide): ```json { "singleQuote": true, "semi": true, "printWidth": 100 } ``` Install Husky, lint-staged, commitlint: ```bash pnpm add -D husky lint-staged @commitlint/cli @commitlint/config-conventional pnpm exec husky init ``` `.husky/pre-commit`: ```bash pnpm exec lint-staged ``` `.husky/commit-msg`: ```bash pnpm exec commitlint --edit "$1" ``` `commitlint.config.cjs` (workspace root): ```js module.exports = { extends: ['@commitlint/config-conventional'] }; ``` `package.json` excerpt: ```json { "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: ```bash pnpm nx affected -t lint test build ``` --- ## 10) Daily workflow ```bash # 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 --project=portal-shell --standalone pnpm nx g @nx/nest:resource --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 |