AngularAtlased. 22.2

Ch. 13Architecture at scale59 / 75

Monorepo, libraries & boundaries

A workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.

architecture ~0 min read Official docs

A monorepo is a single git repository that holds several projects: for example the shop, the admin panel and the libraries they share. It is not a monolith: each application is built and deployed separately. Only the code lives together, with one version of Angular and one commit history.

The gain: a change that touches a library and the two applications that use it lands in a single PR, tested together. The cost: without tooling, each PR rebuilds and retests everything; without rules, anyone imports anything. This page covers both halves: the workspace with the Angular CLI and ng-packagr, then what Nx adds on top of it — the project graph, affected, the cache, and boundaries checked at lint time.

In short
  • A monorepo keeps several apps and libraries in one repository, with a single set of dependency versions.
  • Angular CLI supports several projects in one workspace out of the box (ng generate library). Nx adds the dependency graph, boundaries and caching.
  • A library has a public API: whatever is not exported from its entry file stays internal.
  • “Affected” runs the build and the tests only for what a change touched.
  • Most of the time a folder is enough. A library pays off when it is published or used by several apps.
Why it matters

"Folder, library or npm package" is an architecture decision, not a style one. It determines how quickly you can ship a cross-cutting change and how easily you can break something without noticing. The Angular documentation says it directly: a library forces you to decouple code from the application's logic, but it costs maintenance time — and that pays off when several applications use it.

Monorepo, polyrepo, monolith

The three models differ mainly in the cost of a cross-cutting change. Say the Product model gets a new field, used by the UI library and by both applications:

ModelWhat it looks likeChanging "Product"The price
MonolithOne application, one deploy, folders by domain1 PR, trivialDoes not serve two applications with different release rhythms
Polyrepo + npmEach library in its own repo, published with a version3+ PRs: library → publish → bump in each applicationVersions drift; a fix reaches production when each team "remembers" to
MonorepoApplications + libraries in the same repo, consumed from the workspace1 atomic PR, tested with all its consumersCI has to be smart (affected, cache) and the boundaries have to be enforced

An Angular CLI workspace has a single package.json at the root, so all projects use the same version of Angular, RxJS or CDK — what Nx calls the single version policy. An ng update updates the whole repo once. The price is coordination: no team falls behind, but none can run ahead. If teams really must ship independently, with different rhythms and versions, the next step is micro frontends (which, ironically, often live inside a monorepo too).

Multi-project workspace with the Angular CLI

ng new creates an application at the root by default. For a monorepo you start from an empty workspace (--no-create-application), and each ng generate application or ng generate library adds a project under projects/ and an entry in angular.json. A scoped library (@acme/ui) ends up in projects/acme/ui, and the project name is the package name.

# 1. Empty workspace: no application at the root (createApplication: false)
ng new acme --no-create-application
cd acme

# 2. Projects end up in projects/ (newProjectRoot)
ng generate application shop
ng generate application admin
ng generate library @acme/ui          # → projects/acme/ui, default prefix "lib"

# 3. Each command gets the project name
ng serve shop
ng build admin
ng test @acme/ui
ng build @acme/ui --watch             # incremental rebuild while you work on the library

The important difference is in angular.json: applications use the @angular/build:application builder (esbuild), libraries use @angular/build:ng-packagr, which produces a package in the Angular Package Format and always compiles AOT. For libraries, the default configuration is production.

{
  "newProjectRoot": "projects",
  "projects": {
    "shop": {
      "projectType": "application",
      "root": "projects/shop",
      "sourceRoot": "projects/shop/src",
      "architect": {
        "build": { "builder": "@angular/build:application" },
        "serve": { "builder": "@angular/build:dev-server" }
      }
    },
    "@acme/ui": {
      "projectType": "library",
      "root": "projects/acme/ui",
      "sourceRoot": "projects/acme/ui/src",
      "prefix": "lib",
      "architect": {
        "build": {
          "builder": "@angular/build:ng-packagr",
          "defaultConfiguration": "production",
          "configurations": {
            "production": { "tsConfig": "projects/acme/ui/tsconfig.lib.prod.json" },
            "development": { "tsConfig": "projects/acme/ui/tsconfig.lib.json" }
          }
        }
      }
    }
  }
}
Names matter if you publish

Avoid the ng- prefix (it is reserved for the framework). For public packages the convention is ngx-; for internal ones, a company scope (@acme/…) is the clearest and fits a private registry.

Anatomy of a library

Four files define a library. Each one answers a specific question:

  • ng-package.json — where the build goes (dest) and which file holds the public API (lib.entryFile).
  • package.json — the name, version and dependencies. @angular/* go in peerDependencies: if they were in dependencies, the application could receive a second copy of Angular and break at runtime. "sideEffects": false tells the bundler it can remove everything you don't import.
  • public-api.ts — the contract. What is not exported here is private, so you can refactor it without telling anyone.
  • tsconfig.lib.prod.json — compilationMode: "partial": the partial-Ivy format is stable across Angular versions, and compilation finishes in the application's build, with its own compiler version. Hence the rule: the application must use the same Angular version as the library was built with, or a newer one.
{
  "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
  "dest": "../../../dist/acme/ui",
  "lib": {
    "entryFile": "src/public-api.ts"
  }
}

Secondary entry points

Angular Package Format mainly produces one flat ES module per entry point. A bundler cannot split a module into several chunks, so the entry point is the smallest granularity of lazy loading. That is why Angular Material has one entry point per component (@angular/material/button, …/tabs). The second reason: you isolate special APIs, such as @acme/ui/testing, so they don't reach production by accident.

projects/acme/ui/
├── ng-package.json          → @acme/ui             (primary entry point)
├── src/public-api.ts
├── button/
│   ├── ng-package.json      → @acme/ui/button      (secondary entry point)
│   └── src/public-api.ts
└── testing/
    ├── ng-package.json      → @acme/ui/testing     (harnesses, mocks for tests)
    └── src/public-api.ts

ng-packagr discovers every ng-package.json on its own; you register nothing elsewhere.

A library with a single purpose (an HTTP client, a set of utilities) works well with a single entry point — and @angular/core has just one for the runtime.

Consumption, build and publishing

Inside the same workspace you don't have to publish anything, but you do have to build the library first. ng generate library adds an alias to tsconfig.json that points to dist/, not to the source. The reason: applications and libraries have different builds (esbuild vs ng-packagr), and the same TypeScript can come out as different JavaScript. The documentation explicitly asks that aliases point to the built library. While you work, ng build @acme/ui --watch rebuilds incrementally.

{
  "compilerOptions": {
    "paths": {
      // added automatically by ng generate library — points to the BUILD, not the source
      "@acme/ui": ["./dist/acme/ui"],
      // added by you, for the secondary entry points
      "@acme/ui/*": ["./dist/acme/ui/*"]
    }
  }
}

Publishing means a build in the production configuration (the default) and npm publish from dist — you publish the generated package, not the source folder.

# Distribution build: the production configuration (default for libraries) → partial-Ivy
ng build @acme/ui

# You publish the CONTENTS of dist, not the source folder
cd dist/acme/ui
npm publish            # or: npm publish --registry https://npm.company.com

# The consumer (another repo) installs it like any other package
npm install @acme/ui@^1.4.0
ConsumerHow it gets the codeVersions
Application in the same repoAn alias in tsconfig, always the latest version on the branchNot needed: everyone is on HEAD
Another repo / another clientnpm install @acme/ui from the registryStrict semver, a changelog, correct peerDependencies
Nx does it differently, on purpose

The @nx/angular:lib generator puts the alias in tsconfig.base.json pointing to the source (libs/…/src/index.ts): the library is compiled together with the application, with no separate build step. You add its own build (--buildable / --publishable) only for libraries you publish or for incremental builds. Don't mix the two styles in the same workspace without knowing why.

Nx: the graph and the boundaries

Nx uses the same Angular builders, but keeps the configuration of each project in its own project.json and builds a project graph from analyzing the imports. Everything else rests on that graph: nx graph shows it to you, affected walks it in reverse, and the lint rule checks it on every import. In an existing Angular CLI workspace you start with npx nx@latest init.

# Existing Angular CLI workspace → Nx (keeps the structure)
npx nx@latest init
# …or move the projects into apps/ and libs/
npx nx@latest init --integrated

# Libraries with tags from day one
nx g @nx/angular:lib libs/shop/feature-cart --tags=scope:shop,type:feature
nx g @nx/angular:lib libs/shared/ui --tags=scope:shared,type:ui
nx g @nx/angular:lib libs/shared/ui --publishable --importPath=@acme/ui   # only if you publish it

# The project graph, interactive in the browser
nx graph
nx graph --focus shop-feature-cart

# Run only what is affected by the changes compared to main
nx affected -t lint test build
nx affected -t build --graph        # see the task graph before you run it
nx run-many -t test                 # everything, no filter

Library types

The Nx guide proposes four types — the same roles as in smart vs dumb and in the folder structure, except that now the boundary is a library with a public API:

TypeWhat it containsMay depend on
featurePages and container components for a use case; usually lazyany type
uiPresentational components; the data arrives through inputs, no injected servicesui, util
data-accessCommunication with the backend and state (stores)data-access, util
utilPure functions, often with no framework codeutil
acme/
├── nx.json                     ← cache, targetDefaults, defaultBase
├── tsconfig.base.json          ← "@acme/shop/feature-cart": ["libs/shop/feature-cart/src/index.ts"]
├── apps/
│   ├── shop/project.json       tags: scope:shop,   type:app
│   └── admin/project.json      tags: scope:admin,  type:app
└── libs/
    ├── shop/
    │   ├── feature-catalog/    tags: scope:shop,   type:feature      (smart pages, lazy)
    │   ├── feature-cart/       tags: scope:shop,   type:feature
    │   └── data-access/        tags: scope:shop,   type:data-access  (API + store)
    ├── admin/
    │   ├── feature-users/      tags: scope:admin,  type:feature
    │   └── data-access/        tags: scope:admin,  type:data-access
    └── shared/
        ├── ui/                 tags: scope:shared, type:ui           (dumb components)
        ├── data-access/        tags: scope:shared, type:data-access  (auth, HTTP config)
        └── util/               tags: scope:shared, type:util         (pure functions)

Tags on two dimensions

Each project gets tags in project.json. The combination that works in practice is scope (the domain: scope:shop, scope:admin, scope:shared) × type (the layer: type:feature, type:ui…). The scope answers "who talks to whom", the type answers "which way dependencies flow".

{
  "name": "shop-feature-cart",
  "sourceRoot": "libs/shop/feature-cart/src",
  "projectType": "library",
  "tags": ["scope:shop", "type:feature"]
}

The @nx/enforce-module-boundaries rule

The lint rule takes a list of depConstraints. For an import, Nx takes all the constraints whose sourceTag the source project has, and applies them with AND: shop/feature-cart must satisfy both the scope rule and the type rule. onlyDependOnLibsWithTags requires the target to have at least one of the tags. A project with no matching tag at all is not allowed to depend on anything.

import nx from '@nx/eslint-plugin';

export default [
  ...nx.configs['flat/base'],
  ...nx.configs['flat/typescript'],
  ...nx.configs['flat/javascript'],
  {
    files: ['**/*.ts'],
    rules: {
      '@nx/enforce-module-boundaries': [
        'error',
        {
          allow: [],
          depConstraints: [
            // Dimension 1: the domain (who may talk to whom)
            { sourceTag: 'scope:shop', onlyDependOnLibsWithTags: ['scope:shop', 'scope:shared'] },
            { sourceTag: 'scope:admin', onlyDependOnLibsWithTags: ['scope:admin', 'scope:shared'] },
            { sourceTag: 'scope:shared', onlyDependOnLibsWithTags: ['scope:shared'] },
            // Dimension 2: the type (the direction in which dependencies flow)
            { sourceTag: 'type:app', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
            { sourceTag: 'type:feature', onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:data-access', 'type:util'] },
            { sourceTag: 'type:ui', onlyDependOnLibsWithTags: ['type:ui', 'type:util'] },
            { sourceTag: 'type:data-access', onlyDependOnLibsWithTags: ['type:data-access', 'type:util'] },
            { sourceTag: 'type:util', onlyDependOnLibsWithTags: ['type:util'] },
          ],
        },
      ],
    },
  },
];

Besides the tags, the rule has fixed checks that run before them. Three matter every day, in this order: circular dependencies, importing an application (forbidden), and a static import of a lazy-loaded library (it would pull the chunk into the main bundle). You can see all of them in the demo:

LiveThe workspace graph and the boundary rules (simulation)
appsshopappsadminshopfeature-catalogshopfeature-cartadminfeature-usersshopdata-accessshareduiadmindata-accessshareddata-accesssharedutil

scope:shopscope:sharedscope:adminlazy (loadChildren)

scope:shop, type:feature → scope:admin, type:data-access
Try:
Pick the source and the target, then press “Run lint on the import”. The edge turns green or red, and below you see the rule's message and each constraint applied. The engine reproduces the checks and messages of @nx/enforce-module-boundaries, in the same order; it does not run the real ESLint.

The same rule also accepts finer forms:

// Other useful forms of constraints (same rule, same depConstraints):
[
  // "ui" must not reach data-access even TRANSITIVELY (checks the whole chain)
  { sourceTag: 'type:ui', notDependOnLibsWithTags: ['type:data-access'] },

  // the rule applies only to projects that have BOTH tags
  { allSourceTags: ['scope:shop', 'type:ui'], onlyDependOnLibsWithTags: ['scope:shared'] },

  // npm packages banned for one project type
  { sourceTag: 'type:util', bannedExternalImports: ['@angular/*'] },

  // glob and regex in tags
  { sourceTag: 'scope:*', onlyDependOnLibsWithTags: ['/^scope:(shared|core)$/'] },
]

Mind the difference: notDependOnLibsWithTags checks the whole chain of the target's dependencies, not just the direct one. It is the right tool for "the UI never reaches data-access, not even through detours". For boundaries between folders in a single application, without Nx, see Boundaries.

Affected and cache

nx affected -t test starts from the files changed between --base (by default defaultBase, that is main) and --head, maps them to projects, then walks up the graph along the reverse edges: everything that depends, directly or transitively, on a changed project is affected. In the demo workspace, a change in shop/data-access affects 4 projects (the library, the two shop features and the shop application); one in shared/util affects all 10.

On top of that comes the cache: before running a task, Nx computes a hash of its inputs — the project's sources, its dependencies' (per inputs), the configuration, the command. If the hash has been seen before, Nx replays the result (the terminal output and the files) instead of running it. With a remote cache, a task run once in CI no longer runs on anyone's laptop.

Livenx affected -t test, with cache (simulation)
appsshopappsadminshopfeature-catalogshopfeature-cartadminfeature-usersshopdata-accessshareduiadmindata-accessshareddata-accesssharedutil

changed ✎affectedran ✓from cache

0/10
projects affected
0s
test on affected (vs 3m 51s all)

Click a project in the graph: you change one of its files.

  • The CI terminal shows up here.
Press on shared/util: almost the whole repo becomes affected. Run it, then run it again — the second time everything comes from the cache. Then change shop/feature-cart and run: only feature-cart and shop execute, the rest is a cache hit. “merge into main” moves the base; “nx reset” empties the cache.
{
  "defaultBase": "main",
  "namedInputs": {
    "default": ["{projectRoot}/**/*"],
    "production": ["default", "!{projectRoot}/**/*.spec.ts"]
  },
  "targetDefaults": {
    "build": {
      "dependsOn": ["^build"],
      "inputs": ["production", "^production"],
      "cache": true
    },
    "test": {
      "inputs": ["default", "^production"],
      "cache": true
    },
    "lint": { "cache": true }
  }
}
Granularity makes affected precise

If every application depends on one huge shared library, any change in it affects the whole repo, and affected stops saving anything. Smaller libraries, split by domain and type, mean narrower graphs and more cache hits.

The cache assumes tasks without side effects: the same inputs give the same result. An e2e test that hits a real backend is not cacheable — its result depends on something Nx cannot see.

Folder or library?

The Nx guide on project size sums up the trade-off well. You split into libraries when tests slow you down (affected and cache start to skip pieces), when the code has a different owner (a team, a CODEOWNERS file), or when you want verified boundaries. You don't split what always changes together — the boundary costs navigation and configuration without buying anything. And while the design is still being explored, one project is cheaper; you extract the library once the API has settled.

LiveFolder, internal library or publishable package?
Score: -3
A folder is enough

A folder in the feature (or in shared/) with an index.ts as its public API gives you 80% of the benefits with no configuration. Extract the library when a second consumer appears.

src/app/features/cart/ui/ + lint rule on imports
Tick what is true for your code. The score is a heuristic built on the Nx guide on project size, not a law. The same question at the folder level: the Shared vs feature page.

For the "folder" option, the rules from Shared vs feature and an index.ts as the public API give you most of the benefits, with no extra configuration.

When YES, when NO

Monorepo with libraries
  • Two or more applications share domain code or an internal design system.
  • Frequent cross-cutting changes (API models, auth, theme) that you want to ship atomically, in one PR.
  • Several teams in the same code: tags and the lint rule make the boundaries explicit.
  • A slow CI: affected + cache cut the time in proportion to what you touched.
  • A design system also consumed from outside the repo: a publishable library built with ng-packagr.
Stay with one application and folders
  • One application, one team: domain folders (Folder structure) and a no-restricted-imports rule give you the same boundaries.
  • Teams that must ship completely independently, even with different Angular versions: polyrepo or micro frontends.
  • A library per component: configuration with no benefit. A library per domain and type, not per file.
  • Publishing to npm for code used only inside the repo: versions add ceremony with no value.

Common pitfalls

An alias to the library source in an Angular CLI workspace

"@acme/ui": ["projects/acme/ui/src/public-api.ts"] looks convenient (you no longer build the library), but the application compiles the code with esbuild, while external consumers receive what ng-packagr produces. The bugs appear only for them. Fix: an alias to dist and ng build @acme/ui --watch — or switch to Nx, which is designed to compile from source.

@angular/core in dependencies

The library brings its own copy of Angular, and the application ends up with two instances: DI and change detection break in ways that are hard to diagnose. Fix: everything that is @angular/* goes in peerDependencies.

  • Deep imports (@acme/ui/src/lib/button/button) that bypass public-api.ts: the first internal refactor breaks the consumers. Fix: export from the public API or create a secondary entry point; forbid the pattern with no-restricted-imports.
  • Relative imports between entry points of the same library: ng-packagr builds each entry point separately and the build fails. Fix: import by the package name (@acme/ui).
  • Projects without tags, "fixed" with sourceTag: '*' → '*': the rule no longer checks anything. Fix: --tags at generation time, and the tags checked at review.
  • A shared library as a junk drawer that everything depends on: affected = the whole repo on every PR. Fix: split it by domain and type.
  • Caching non-deterministic tasks (e2e against a real backend, tests that read the time): Nx replays a result that is no longer true. Fix: no cache: true on those targets, or complete inputs.
  • A static import over a loadChildren: the lazy chunk disappears into the initial bundle. The Nx rule catches it; without Nx, check the bundle (see Performance).

Cheat sheet

ANGULAR CLI (multi-project)
  ng new acme --no-create-application        empty workspace
  ng g application shop | ng g library @acme/ui
  ng build @acme/ui [--watch]                 libraries are built BEFORE they are consumed
  tsconfig paths → ./dist/acme/ui             (+ "@acme/ui/*" for secondary entry points)
  cd dist/acme/ui && npm publish              only what leaves the repo gets published

LIBRARY
  ng-package.json        dest + lib.entryFile          (secondary: only "lib")
  public-api.ts          the public API — the rest is private
  package.json           peerDependencies for @angular/*, "sideEffects": false
  compilationMode        "partial" for npm

NX
  nx graph                         the project graph
  nx affected -t lint test build   only the affected projects (transitive dependents included)
  "cache": true in targetDefaults  hash over inputs → replay on the second run
  tags + @nx/enforce-module-boundaries → boundaries enforced at lint time
  types: feature → ui, data-access, util · ui → ui, util · data-access → data-access, util · util → util

Ch. 13 · 0 of 4 complete

Finished this lesson?

Mark it and it shows up on your map: in the contents and on the home page.

Marks itself as done once you have read the whole lesson.

  1. Micro frontends
  2. Web Components with Angular Elements
  3. Monorepo, libraries & boundaries
  4. Hexagonal architecture (ports & adapters)
See your whole map

All 75 pages · type to filter

  • 01What's new in Angular 21 & 22All the important changes in v21 and v22, briefly, with a link to the page that explains each one.Start
  • 01Your first app, step by stepA reading list built from an empty component to a finished app — signals, @for, inputs and outputs, a service and a form, one step at a time.Start
  • 01Quick decisions“Where do I put this?”, “Signal or Observable?”, “Shared or feature?” — short answers to everyday questions.Start
  • 01Angular interview questions60 real interview questions for junior, mid and senior, with the answer to give, the follow-up and the red flags. Plus a mock interview.Start
  • 02Standalone & bootstrapHow an app starts without NgModule: bootstrapApplication, app.config.ts, imports in the component.Modern fundamentals
  • 02Data binding & eventsInterpolation, property and attribute bindings, events with $event and key filters, and how [( )] is built — what goes into the DOM and what comes back.Modern fundamentals
  • 02Control flow: @if, @for, @switchThe block syntax that replaces *ngIf/*ngFor/ngSwitch, with track, @empty and exhaustive checks.Modern fundamentals
  • 02@let and the new template expressionsLocal variables with @let, arrow functions, spread, regex, instanceof, comments inside tags.Modern fundamentals
  • 02@defer — lazy loading in templatesLoad heavy components only when needed: triggers, prefetch, placeholder, loading, error.Modern fundamentals
  • 02Class & style bindingsWhy [class.x] and [style.x] replaced ngClass and ngStyle (with automatic migration from v21).Modern fundamentals
  • 02Component styles & encapsulation:host, the three encapsulation modes, theming with CSS variables, and why ::ng-deep is a last resort.Modern fundamentals
  • 02Pipes: when and howPure vs impure pipes, custom pipes, and how you choose between a pipe, computed() and a method.Modern fundamentals
  • 03signal() and computed()The reactive primitives: writable state, derived state, equality, untracked and immutability rules.Signals & reactivity
  • 03effect() — when YES, when NOSide effects synchronised with signals: localStorage, logging, imperative APIs. And their traps.Signals & reactivity
  • 03linkedSignal() — editable derived stateA signal that resets when its source changes, but that you can also set manually.Signals & reactivity
  • 03resource, rxResource, httpResourceAsync data as signals: status, value, error, reload, automatic cancellation. Stable since v22.Signals & reactivity
  • 03debounced() — delayed signalsThe “debounce” version of a signal, without RxJS. Perfect for search.Signals & reactivity
  • 03Lists: search, filter, sort, paginateA chain of computed signals that recalculates only what changed, with the filters kept in the URL so a link reopens the same view.Signals & reactivity
  • 03Observables from zeroWhat an Observable is, subscribe and unsubscribe, the operators you use every week, cold vs hot — and when a signal is enough.Signals & reactivity
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — and when RxJS remains the right choice.Signals & reactivity
  • 04input() and output()Inputs as signals (required, transform, alias) and typed outputs. Data down, events up.Components: communication
  • 04model() — two-way bindingBanana-in-a-box [( )] with a single API. When to use it, and when input + output is better.Components: communication
  • 04viewChild & contentChildQueries as signals: viewChild, viewChildren, contentChild, contentChildren, the read option.Components: communication
  • 04Lifecycle & DestroyRefWhich hooks still matter in the signals era, afterNextRender, DestroyRef and typed ngOnChanges.Components: communication
  • 04Content projectionng-content with select, multiple slots, default content and the trap of conditional projection.Components: communication
  • 04ng-template & ngTemplateOutletTemplates as values: render props in Angular, typed context and customising components.Components: communication
  • 05Smart vs DumbContainer vs presentational: who holds the state, who only displays, and how to split them properly.Component design
  • 05Generic components <T>A correctly typed DataTable<T> or Select<T>, with templates for cells.Component design
  • 05Dynamic componentsNgComponentOutlet, createComponent with inputBinding/outputBinding, and a dashboard built from config.Component design
  • 05Directives & host compositionAttribute directives, the host: {} metadata and the Directive Composition API (hostDirectives).Component design
  • 05Custom form controlsFormValueControl (Signal Forms) vs ControlValueAccessor: a star rating, in both variants.Component design
  • 05Change detectionOnPush is the default from v22, zone.js is gone from v21. What triggers rendering and how not to get burned.Component design
  • 06inject() & @Service()The new @Service() decorator from v22, inject() vs the constructor, and the injection context.Dependency Injection
  • 06Providers & the injector hierarchyRoot vs route vs component, useClass/useValue/useFactory/useExisting, and the optional/self/skipSelf modifiers.Dependency Injection
  • 06InjectionToken & provideX()Typed configuration, provideFeature() functions, makeEnvironmentProviders and initializers.Dependency Injection
  • 06injectAsync() — lazy servicesLoad a heavy service only when you need it, with prefetch on idle.Dependency Injection
  • 07Folder structurecore / shared / features, what goes into each, and what a feature looks like inside.Application architecture
  • 07Shared vs featureWhen to move a component into shared and (especially) when NOT to. A concrete checklist.Application architecture
  • 07Interfaces & modelsInline interface or separate file? DTO vs domain model, type vs interface, where to put them.Application architecture
  • 07State management with signalsSimple signal stores, facades, local vs global state, and when you need NgRx.Application architecture
  • 07Style guide & namingThe current official conventions: file names without suffixes, protected, readonly, member order.Application architecture
  • 07Barrels, aliases & boundariesindex.ts, path aliases, a feature’s public API and dependency rules that can be checked automatically.Application architecture
  • 07Errors: @boundary & ErrorHandlerThe new @boundary block from v22.2, the global ErrorHandler, HTTP errors and errors from resource.Application architecture
  • 08Modern routing & lazy loadingloadComponent, child routes, parameters as inputs and parameter inheritance (the new default in v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate and resolve as functions, with redirects via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — the basicsForms built on signals: form(), [formField], schema-based validation. Stable since v22.Forms
  • 09Signal Forms — advancedCustom and cross-field validation, async/HTTP with debounce, dynamic lists, disabled/hidden, reusable schemas.Forms
  • 09Typed Reactive FormsTyped FormGroup, nonNullable, FormArray — and how they coexist with Signal Forms.Forms
  • 10HttpClient, interceptors & the API layerHttpClient without setup (v21), Fetch by default (v22), functional interceptors and an API layer with DTO mapping.HTTP & data
  • 11Performance — a checklistWhat really matters: OnPush + signals, track, @defer, lazy routes, images, bundles and hydration.Quality & performance
  • 11Testing with VitestVitest is the default runner from v21. TestBed, components with signals, services, HTTP and harnesses.Quality & performance
  • 11DevTools: look inside your appChrome DevTools and Angular DevTools step by step: what each panel shows, with exercises that work on this site.Quality & performance
  • 11Angular AriaHeadless, accessible components that you style however you like: tabs, accordion, menu, listbox...Quality & performance
  • 12SSR, prerender & render modesWhen to render on the server, when at build time and when only in the browser: RenderMode per route, app.routes.server.ts and the real costs.SSR & hydration
  • 12Hydration: event replay & incrementalHow the browser takes over server HTML without re-rendering it: full hydration, event replay, @defer (hydrate on …) and mismatch errors.SSR & hydration
  • 12Server-compatible codewindow doesn’t exist on the server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, the HTTP cache and PendingTasks.SSR & hydration
  • 13Micro frontendsWhen it is worth splitting an app into several deployments and when it is just complexity: Native Federation, Module Federation, iframes, Web Components.Architecture at scale
  • 13Web Components with Angular ElementscreateCustomElement: an Angular component used as an HTML tag anywhere, plus communication between apps through events and BroadcastChannel.Architecture at scale
  • 13Monorepo, libraries & boundariesA workspace with several apps and libraries: ng generate library, Nx, secondary entry points, publishing and dependency rules.Architecture at scale
  • 13Hexagonal architecture (ports & adapters)A domain without Angular, ports as abstract classes and interchangeable adapters: HTTP, localStorage or in-memory, without touching the UI.Architecture at scale
  • 14Signals under the hoodThe reactive graph: producers and consumers, push-pull, why computed is lazy and glitch-free, the diamond problem and when effects run.Advanced reactivity
  • 14Advanced RxJS: higher-order operatorsswitchMap, mergeMap, concatMap and exhaustMap on a live diagram, plus retry with backoff, shareReplay and how to avoid leaks.Advanced reactivity
  • 14NgRx SignalStoreLarge-scale state management on signals: withState, withComputed, withMethods, rxMethod, entities and hooks — and when a simple store is enough.Advanced reactivity
  • 14State machines with signalsNo more impossible states: a typed machine on signals, with transitions, guards and a live state diagram.Advanced reactivity
  • 15WebSockets: real time that survives the networkA typed socket as signals: reconnect with backoff and jitter, heartbeat, an outbox while offline, and SSR safety.Real time & offline
  • 15Optimistic updates & rollbackAn instant UI that stays honest: snapshot and rollback, out-of-order responses, idempotency keys.Real time & offline
  • 15Offline-first: Service Worker & cachingThe Angular Service Worker: caching strategies, update prompts and an outbox for changes made offline.Real time & offline
  • 16Web WorkersHeavy computation without blocking the UI: a real worker in the page, message passing and when it is not worth it.Advanced UI & performance
  • 16Huge lists: virtual scroll100,000 rows without lag: CDK virtual scroll, itemSize, trackBy and the alternatives (pagination, infinite scroll).Advanced UI & performance
  • 16Modern animations & View Transitionsanimate.enter and animate.leave instead of the deprecated @angular/animations, CSS transitions and View Transitions on navigation.Advanced UI & performance
  • 16Memory leaks: finding and fixing themSubscriptions, timers and listeners that outlive their component — how to spot them in DevTools and close them.Advanced UI & performance
  • 17Security: XSS, sanitisation & CSPWhat Angular does automatically against XSS, when bypassSecurityTrust becomes dangerous, plus CSP with nonce, Trusted Types and XSRF.Security & globalisation
  • 17Authentication: tokens & refreshAn interceptor that attaches the token, refreshes once on 401 and replays the queued requests — plus where you keep the token.Security & globalisation
  • 17OAuth 2.1 & OpenID Connect with PKCEThe authorization code flow with PKCE, step by step: verifier and challenge, state and nonce, the ID token, and why a BFF is safer.Security & globalisation
  • 17Internationalisation (i18n)$localize, the i18n attribute, ICU plural/select, a build per language vs runtime translations, and locale-based formatting.Security & globalisation