Eine gute Ordnerstruktur beantwortet zwei Fragen sofort: „Wo finde ich X?“ und „Wo lege ich Y ab?“. Die aktuelle Empfehlung (und die, die genau diese Seite nutzt) ist, nach Domänen (Features) zu organisieren, nicht nach Dateityp — mit zwei übergreifenden Bereichen: core und shared.
Die empfohlene Struktur
src/app/
├── app.ts · app.config.ts · app.routes.ts
│
├── core/ ← einmal pro Anwendung: Auth, Layout, Fehler, Interceptors
│ ├── auth/
│ ├── layout/
│ └── error-handling/
│
├── shared/ ← wiederverwendbar, OHNE Domäne
│ ├── ui/ ← generische Dumb-Komponenten
│ ├── directives/
│ ├── pipes/
│ └── util/ ← reine Funktionen
│
└── features/ ← die fachlichen Domänen
├── products/
│ ├── products.routes.ts
│ ├── pages/ ← Smart (geroutet)
│ ├── ui/ ← Dumb, domänenspezifisch
│ ├── data-access/ ← api + store
│ └── models/ ← Typen, DTOs, Mapper
├── cart/
└── checkout/Die Wurzel der Anwendung. Hier liegen nur die Startdateien und die drei Bereiche: core, shared, features.
Die drei Bereiche, jeweils in einem Satz
| Bereich | Die Testfrage | Darf importieren aus |
|---|---|---|
core/ | „Wird es einmal auf Anwendungsebene genutzt, oder ist es Infrastruktur?“ | shared |
shared/ | „Würde es in ein anderes Projekt kopiert genauso funktionieren?“ | nur anderes shared |
features/x/ | „Geht es um die Domäne X?“ | shared, core (Services) |
Innerhalb eines Features
Innerhalb eines Features folgen die Unterordner den Rollen aus Smart vs Dumb: pages (Smart, geroutet), ui (Dumb), data-access (API + Store) und models (Typen). Lege sie nicht von Anfang an an — ein kleines Feature kommt gut flach aus:
// Ein kleines Feature? Keine Unterordner — flach, bis es unhandlich wird (~6–8 Dateien)
features/settings/
├── settings.routes.ts
├── settings-page.ts
├── settings-page.html
├── settings-store.ts
├── notification-toggle.ts
└── settings.ts ← die TypenLazy Loading pro Feature
// Jedes Feature ist lazy: ein einziger Einstiegspunkt (die Routes-Datei)
export const routes: Routes = [
{ path: '', loadComponent: () => import('./features/home/home-page') },
{ path: 'products', loadChildren: () => import('./features/products/products.routes') },
{ path: 'cart', loadChildren: () => import('./features/cart/cart.routes') },
{
path: 'admin',
canMatch: [adminGuard],
loadChildren: () => import('./features/admin/admin.routes'),
},
{ path: '**', loadComponent: () => import('./core/layout/not-found') },
];Anti-Pattern: Ordner nach Typ
// ✗ Organisation „nach Typ“ — skaliert schlecht
src/app/
├── components/ (120 Dateien aus allen Domänen vermischt)
├── services/ (40)
├── models/ (60)
└── pipes/
// Um „checkout“ zu ändern, öffnest du 4 Ordner und suchst zwischen Hunderten von Dateien.
// Sauberes Lazy Loading ist nicht möglich, Grenzen zwischen Domänen lassen sich nicht erzwingen.Für große Monorepos
// Ein großes Monorepo (Nx): dieselben Ideen, aber als Bibliotheken mit Tags und per Lint geprüften Regeln
libs/
├── products/
│ ├── feature-list/ type:feature (Smart-Seiten)
│ ├── feature-detail/ type:feature
│ ├── ui/ type:ui (Dumb)
│ ├── data-access/ type:data-access (api, store)
│ └── util/ type:util
└── shared/
├── ui/ scope:shared, type:ui
└── util/
Typische Regeln: feature → ui, data-access, util ✓ ui → util ✓ ui → data-access ✕Goldene Regeln
- Colocation: Was sich gemeinsam ändert, bleibt zusammen (die Komponente, ihr Template, Styles, Test und Typen).
- Flach, bis es wehtut: Lege einen Unterordner an, wenn es zu viele Dateien gibt, nicht „für die Zukunft“.
- Features importieren sich nicht gegenseitig. Kommuniziert über
core(globale Stores),sharedoder die URL. Siehe Grenzen. - Jedes Feature hat genau einen Einstiegspunkt: seine Routes-Datei.
- Der Ordnername = der Domänenname, Plural oder Singular — aber konsistent.
Häufige Stolperfallen
sharedals „Abstellkammer“: Alles, wofür du keinen Platz weißt, landet dort. Klare Regeln findest du unter Shared vs Feature.coremit Komponenten, die seitenübergreifend wiederverwendet werden:coreist für Dinge gedacht, die einmal genutzt werden (Shell, Header), nicht für gemeinsame UI.- Ein einziges Feature „common“ oder „general“: Das ist ein getarntes
sharedmit vermischten Domänen.