Eine Angular-Anwendung besteht nicht nur aus Komponenten. Drumherum liegt eine klar definierte Werkzeug-Schicht aus Workspace, Builder-Konfiguration, Bootstrap-Logik und Injector-Hierarchie. Wer diese Architektur einmal sauber durchdrungen hat, versteht, warum ng new genau so erzeugt, wie es erzeugt – und an welchen Stellen sich der Bauplan später erweitern lässt. Dieser Artikel zeichnet das Gesamtbild von angular.json über main.ts bis hinunter zur Change Detection.

Workspace und Projects

Die oberste Einheit in Angular ist der Workspace. Er wird durch eine angular.json im Wurzelverzeichnis markiert und kann ein oder mehrere Projects enthalten. Ein Project ist entweder eine Anwendung (projectType: "application") oder eine Library (projectType: "library"). So lassen sich App, gemeinsam genutzte UI-Library und E2E-Suite in einem Workspace zusammen pflegen.

Eine typische Verzeichnisstruktur sieht so aus:

Bash my-app/
my-app/
  angular.json          # Workspace-Manifest
  package.json
  tsconfig.json         # Basis-TS-Config
  tsconfig.app.json
  tsconfig.spec.json
  public/               # statische Assets (ab v17 Default)
  src/
    main.ts             # Bootstrap-Eintrittspunkt
    index.html
    styles.scss
    app/
      app.component.ts
      app.config.ts     # Standalone-Provider
      app.routes.ts

Die angular.json beschreibt für jedes Project unter architect die ausführbaren Targets: build, serve, test, lint. Jedes Target verweist auf einen Builder und liefert dessen Optionen.

Die Builder-API: application vs. browser

Angular nutzt eine generische Builder-API. In der Praxis sind heute zwei Builder relevant:

  • @angular/build:application – der moderne, esbuild-basierte Builder. Standard für neue Projekte seit v17. Schneller HMR, kleinere Bundles, integriertes SSR.
  • @angular-devkit/build-angular:browser – der ältere, webpack-basierte Builder. Für Bestands-Projekte weiter verfügbar, aber nicht mehr Default.
JSON angular.json (Auszug)
{
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "my-app": {
      "projectType": "application",
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "architect": {
        "build": {
          "builder": "@angular/build:application",
          "options": {
            "outputPath": "dist/my-app",
            "browser": "src/main.ts",
            "index": "src/index.html",
            "tsConfig": "tsconfig.app.json",
            "assets": [{ "glob": "**/*", "input": "public" }],
            "styles": ["src/styles.scss"]
          }
        }
      }
    }
  }
}

Bootstrap: Standalone vs. NgModule

Jede Angular-Anwendung beginnt in src/main.ts. Hier wird die Wurzelkomponente in den DOM gehängt und die Provider-Landschaft aufgespannt. Es gibt heute zwei gleichwertige Wege – beide werden offiziell unterstützt.

Standalone-Bootstrap (Default seit v19)

TypeScript main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';

import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';
import { authInterceptor } from './app/core/auth.interceptor';

bootstrapApplication(AppComponent, {
    providers: [
        provideRouter(routes),
        provideHttpClient(withInterceptors([authInterceptor])),
        provideAnimationsAsync()
    ]
}).catch(err => console.error(err));

bootstrapApplication startet ohne AppModule. Provider werden über funktionale provide…-Helfer registriert. Tree-Shaking ist hier besonders wirksam, weil ungenutzte Features gar nicht erst importiert werden.

NgModule-Bootstrap (weiterhin First-Class)

NgModules sind kein Legacy. Für viele große Bestands-Apps und stark modular geschnittene Feature-Bereiche bleiben sie eine sinnvolle Option und sind voll supported.

TypeScript main.ts
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic()
    .bootstrapModule(AppModule)
    .catch(err => console.error(err));
TypeScript app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { HttpClientModule } from '@angular/common/http';
import { RouterModule } from '@angular/router';

import { AppComponent } from './app.component';
import { routes } from './app.routes';

@NgModule({
    declarations: [AppComponent],
    imports: [
        BrowserModule,
        BrowserAnimationsModule,
        HttpClientModule,
        RouterModule.forRoot(routes)
    ],
    bootstrap: [AppComponent]
})
export class AppModule {}

Die Tabelle zeigt die direkte Korrespondenz:

AufgabeStandalone-ProviderNgModule-Pendant
RoutingprovideRouter(routes)RouterModule.forRoot(...)
HTTPprovideHttpClient()HttpClientModule
AnimationenprovideAnimationsAsync()BrowserAnimationsModule
Forms (Reactive)provideForms() o. ImportsReactiveFormsModule

Dependency Injection: Environment- und Element-Injector

Angular kennt zwei parallele Injector-Hierarchien:

  • EnvironmentInjector – aufgespannt durch Bootstrap, Lazy-Routes und EnvironmentProviders. Hier leben Services wie HttpClient, Router, globale Stores.
  • ElementInjector – pro Komponenten-Instanz. Steuert Komponenten-lokale Services über das providers-Array im @Component-Dekorator.

Beim inject()-Aufruf wandert Angular die Hierarchie von der konkreten Komponente nach oben, bis es einen Provider findet. Das erlaubt scoped overrides: ein generischer Service auf Workspace-Ebene, ein spezialisierter pro Feature-Route, ein Mock im Test.

TypeScript user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({ providedIn: 'root' })
export class UserService {
    private http = inject(HttpClient);

    load(id: string) {
        return this.http.get(`/api/users/${id}`);
    }
}

Change Detection: Zone.js und Zoneless

Damit Änderungen im Modell zu Re-Renderings führen, braucht Angular einen Trigger. Klassisch übernimmt das Zone.js durch das Patchen aller Browser-APIs (Events, Timer, XHR). Nach jedem solchen Trigger läuft die Change Detection einmal durch den Komponentenbaum.

Seit v18 (experimentell) und mit weiterer Reife in v20/v21 gibt es den Zoneless-Modus: Statt globaler Zone-Patches stoßen Signals und explizite Marker die Change Detection an. Das spart Bundle-Size (kein Zone.js) und liefert deterministischere, oft schnellere Updates.

In Kombination mit ChangeDetectionStrategy.OnPush (in v22 als Default vorgesehen) entsteht ein Modell, das näher an React/Solid liegt: Re-Render nur dort, wo sich tatsächlich abhängige Werte ändern.

Interessantes

Warum überhaupt ein DI-Container?

Dependency Injection wirkt auf den ersten Blick wie Overhead, ist aber der Hebel für Testbarkeit und Modularität. Ein HttpClient lässt sich im Test gegen einen Mock tauschen, ohne eine einzige Komponente anzufassen. Genau das macht Angular-Apps in großen Teams so wartbar.

angular.json vs. project.json (Nx)

Wer Nx einsetzt, sieht statt einer großen angular.json viele kleine project.json-Dateien pro Library und App. Das ist kein Bruch mit Angular: Nx liest dieselbe Builder-API, splittet die Konfiguration aber feiner und ergänzt eigene Executors.

Standalone-Bootstrap startet schneller

Ohne Modul-Auflösung beim Start spart der Standalone-Bootstrap messbar Initialisierungszeit. Lazy-Loaded-Routes mit loadComponent oder loadChildren ziehen ihre Provider erst dann nach, wenn die Route tatsächlich aktiv wird – ideal für Time-to-Interactive.

Workspace mit mehreren Apps

Ein Workspace kann beliebig viele Apps und Libraries enthalten. ng generate application admin legt eine zweite App neben der bestehenden an, beide teilen sich node_modules, TS-Config und Lint-Regeln. Für Monorepos ohne Nx ist das oft schon ausreichend.

Tree-Shaking durch funktionale Provider

Das Muster provideRouter(…) statt RouterModule.forRoot(…) ist nicht nur kosmetisch. Funktionale Provider erlauben dem Bundler präziseres Tree-Shaking: Nicht genutzte Router-Features (etwa Hash-Location) landen erst gar nicht im Bundle.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Grundlagen

Zur Übersicht