W9 built one application: kladde-angular, one project in one folder. This course asks what happens when there are two applications (the office application and the public booking), several domains (contacts, appointments, tenders) and more than one team. The lesson first clarifies when a monorepo is the answer, then sets up the workspace kladde-workspace with Nx and pulls the SPA from W9 into it as kladde-buero. Rules between libraries follow in lesson 0102, the cache in 0103. W9 is a prerequisite; we write decisions down as ADRs, as learned in K4 lesson 4.1.3.

State of the checks. Everything shown here with a result was run in a throwaway workspace outside the repository: Node 24.22.3, npm 11.7.0, Nx 24.0.6 (nx, @nx/angular), Angular 22.2 (@angular/core 22.2.6), TypeScript 6.0.3, Vitest 4.1.12; nx build, nx test, nx lint and nx graph --file ended with exit 0. The "SPA from W9" here is the reference application of the writing (a fresh ng new project with routing and SCSS, two tests), not your finished Kladde SPA. Not run: nx init and nx import (for existing repositories), nx graph in the browser, ng serve in the workspace. Nx's statements come from the pages on cache, affected and module boundaries (read, see Further reading); the reasons against a monorepo are our judgment, not a source. An attempt with --preset=angular-monorepo gave us, in an environment with the variables of an AI agent, a template with the sample applications shop and api; whether that is the case for you as well is open. We use the preset apps.

What a monorepo solves and what it costs

A monorepo is a repository with several projects (applications and libraries) that are allowed to know each other. It is not a goal but a tool: code that is changed together lives in one commit and one check.

What it solves What it costs
A change to a shared library and its users is one pull request, not version jumps between repositories. The repository grows: cloning, searching, history. We do not name a size at which it becomes too big.
One version of Angular and RxJS for everyone; upgrades happen once. Everyone is tied to the same Angular version; an application that has to stay behind slows down the rest.
Lint, format and boundaries in one place; reuse without publishing. Without rules, a tangle grows: every project may import everything (lesson 0102).
One tool sees the whole graph and builds only what is affected. CI needs this tool and the whole Git history (lesson 0103).

On top of that come access rights (whoever sees the repository sees everything), ownership (a team that owns its folder needs rules such as CODEOWNERS) and publishing several packages with versions of their own (lesson 0302). For the Kladde this holds: two applications share contact and appointment shapes, building blocks and helper functions, and one team changes them together. A monorepo carries that. With two teams that have their own schedules and Angular versions, the question would come out differently; lesson 0303 covers the counterpart, micro frontends.

Nx at a glance

Nx is a tool for monorepos (nx and @nx/angular 24.0.6, MIT license according to the npm registry). Four building blocks:

  • Projects and the project graph. Every application and library is a project with a name, a folder, tags and tasks. From the imports, Nx computes the graph: who depends on whom.
  • Tasks (targets). build, test, lint, serve. nx build kladde-buero runs one, nx run-many -t lint test build runs all of them for all projects.
  • Cache. Before a task, Nx forms a hash from its inputs; if the result already exists, it is restored (lesson 0103).
  • Generators. nx generate creates applications, libraries and components from a template and enters them in the configuration.

Angular CLI workspace or Nx? The Angular CLI also knows workspaces with several projects. Nx builds on the same blocks: the build task of our application calls the builder @angular/build:application, the same one as ng build in W9. Nx adds the graph, cache, affected, boundary rules and generators for libraries. Whoever has one application does not need that. Whether ng keeps working in the Nx workspace, we did not check; the course uses nx.

Creating the workspace

Check the version first; Angular 22 requires Node ^22.22.3, ^24.15.0 or ^26.0.0 (angular.dev, "Versions"). The course names the 24 line.

PowerShell
node --version
npx create-nx-workspace@24.0.6 kladde-workspace --preset=apps --nxCloud=skip `
    --aiAgents=none --analytics=false --interactive=false --defaultBase=master `
    --formatter=prettier --linter=eslint

--nxCloud=skip does without the remote cache (lesson 0103), --defaultBase=master names the main branch that affected compares against (for you, perhaps main). The result is an empty workspace with npm workspaces: nx.json, package.json, no Angular yet. Nx reads the history, so initialize Git, then Angular support and the application follow:

PowerShell
cd kladde-workspace
git init -b master
git add -A
git commit -m "chore: initial workspace"
npx nx add @nx/angular
npx nx generate @nx/angular:application apps/kladde-buero --name=kladde-buero `
    --prefix=kl --style=scss --e2eTestRunner=none --linter=eslint --no-interactive

nx add reported "Package @nx/angular added successfully" for us; check that @nx/angular is in the devDependencies (to add it: npm install -D @nx/angular@24.0.6). The prefix kl and SCSS are the course's settings, as in W9; according to the generator's help, the default for --e2eTestRunner is Playwright, and we do not need an E2E project here. The generator writes apps/kladde-buero/project.json, the tsconfig files, src/ with main.ts, app.config.ts, app.routes.ts, app.ts, and extends nx.json and eslint.config.mjs. As the test runner, it enters Vitest (@angular/build:unit-test, like ng test in W9).

PowerShell
npx nx build kladde-buero
npx nx test kladde-buero
npx nx lint kladde-buero

For us: build exit 0, output in dist/apps/kladde-buero, with a warning about the style budget of nx-welcome.ts (the template's welcome page, which goes away shortly); test "Test Files 1 passed, Tests 2 passed" and "Cache: 0/1 hit (0%)"; lint without findings.

The configuration

project.json describes one project (shortened):

JSON
{
  "name": "kladde-buero",
  "projectType": "application",
  "prefix": "kl",
  "sourceRoot": "apps/kladde-buero/src",
  "tags": [],
  "targets": {
    "build": {
      "executor": "@angular/build:application",
      "outputs": ["{options.outputPath}"],
      "options": { "outputPath": "dist/apps/kladde-buero" }
    },
    "test": { "executor": "@angular/build:unit-test", "options": { "watch": false } }
  }
}

outputs names the files that the cache restores. The tags are still empty; their purpose follows in lesson 0102.

nx.json applies to the workspace; the generator filled three sections:

  • namedInputs determines which files are inputs of a task: default (everything in the project plus shared files) and production (without test files and lint configuration). A build that reads production does not run again when only a test changes.
  • targetDefaults gives all tasks of a builder shared values. For @angular/build:application, there are cache: true, dependsOn: ["^build"] (first the build tasks of the projects this one depends on) and inputs: ["production", "^production"].
  • plugins (here @nx/eslint/plugin) derive tasks from configuration files: a library needs no lint in its project.json. nx show project <name> shows all tasks that Nx sees.

On strict: the template sets "strict": false in tsconfig.base.json, but true in the tsconfig.json of the application and the libraries. Our rule is strict TypeScript everywhere: set it to true at the root. In our workspace with six projects (as it looks after lesson 0102), everything kept running with exit 0 afterward, and none of the 13 tasks came from the cache: the root configuration is an input of all tasks.

Pulling the SPA from W9 in

The template's application is only a scaffold; the content is the SPA from W9. Copying is the short way we took (source web/kladde-angular, target apps/kladde-buero):

PowerShell
Remove-Item -Recurse -Force apps\kladde-buero\src\app
Copy-Item ..\kladde-angular\src\app apps\kladde-buero\src\app -Recurse
Copy-Item ..\kladde-angular\src\main.ts apps\kladde-buero\src\main.ts -Force
Copy-Item ..\kladde-angular\src\styles.scss apps\kladde-buero\src\styles.scss -Force

nx import can fetch a whole repository including its history; we did not run it and did not read its page: open. After that, three adjustments by hand: the selector in app.ts and in the tests is kl-root (the reference application had app-root), the name in the title and the test is kladde-buero, and additional dependencies (MSAL from W9 0301, an OpenAPI client) go into the package.json at the root: an Nx workspace has one version of each package for all projects. The proxy proxy.conf.json (W9 0101) belongs in apps/kladde-buero and in the serve task; we did not run that.

For us, the runs afterward built and tested with exit 0 again: build with a main bundle of about 217 kB (raw size, only a data point, not a yardstick), test 2 of 2, lint without findings. From now on the office application is called kladde-buero; kladde-angular stays as a template until the workspace shows the same pages.

The project graph

PowerShell
npx nx graph --file=graph.json
npx nx show projects

nx graph without an argument opens the drawn graph in the browser (not run); with --file, JSON with nodes and dependencies is produced. With one application, the graph is a single node; the first edges appear in lesson 0102.

Exercise 1

Create the workspace, pull the SPA in, record the decision.

  1. Check node --version, create kladde-workspace with the preset apps, initialize Git, add @nx/angular and generate kladde-buero with prefix kl and SCSS. Set strict in tsconfig.base.json to true.
  2. Replace the application's source with that of your W9 SPA, adjust the selector and title, move additional dependencies into the root package.json.
  3. Run nx build kladde-buero, nx test kladde-buero and nx lint kladde-buero (read the exit code each time) and print the graph with nx graph --file=graph.json.
  4. Write docs/adr/0001-monorepo.md following K4 4.1.3: context, decision (Nx monorepo), consequences and one alternative with a reason.

How to check: all runs end with exit=0, graph.json contains the node kladde-buero, and in the ADR the consequences name at least one disadvantage, not only advantages.

Solution 1

  • create-nx-workspace asks questions: with --interactive=false and all the options from the text it does not ask; --nxCloud=skip answers the question about Nx Cloud.
  • The generator reports that @nx/angular is missing: npm install -D @nx/angular@24.0.6.
  • npm prints "Unsupported engine": something in the chain runs with a different Node version than you think. For us, npm ran with Node 22.12.0 and printed these notices, while nx ran under 24.22.3; check which Node each tool starts with.
  • The SPA's tests do not find app-root or find the old title: adjust the selector and title in app.ts and app.spec.ts. A missing package (MSAL, a client) belongs in the root package.json.
  • An ADR example: "The public booking (kladde-buchung) needs the same contact shapes and building blocks as the office. We take a monorepo with Nx. Consequence: one Angular version; without boundary rules, a tangle looms. Rejected: two repositories with a published package, because every change to the shapes would mean two pull requests and a version."

What you can do now

  • You say what a monorepo solves and what it costs, and justify why it pays off for the Kladde.
  • You name the project graph, tasks, cache and generators and read nx.json (namedInputs, targetDefaults, plugins) and project.json.
  • You create an Nx workspace with an Angular application and check it with build, test and lint, exit code included.
  • You pull the SPA from W9 in as kladde-buero and record the decision as an ADR.

Further reading

The Kladde: the workspace kladde-workspace stands, kladde-buero builds, tests and lints. Next comes lesson 0102: libraries by type and domain, tags and a lint run that rejects a forbidden import.