O structură bună de foldere răspunde instant la două întrebări: „unde găsesc X?” și „unde pun Y?”. Recomandarea actuală (și cea pe care o folosește chiar acest site) e organizarea pe domenii (feature-uri), nu pe tipuri de fișiere, cu două zone transversale: core și shared.
Structura recomandată
src/app/
├── app.ts · app.config.ts · app.routes.ts
│
├── core/ ← o dată per aplicație: auth, layout, erori, interceptori
│ ├── auth/
│ ├── layout/
│ └── error-handling/
│
├── shared/ ← refolosibil, FĂRĂ domeniu
│ ├── ui/ ← componente dumb generice
│ ├── directives/
│ ├── pipes/
│ └── util/ ← funcții pure
│
└── features/ ← domeniile business-ului
├── products/
│ ├── products.routes.ts
│ ├── pages/ ← smart (rutate)
│ ├── ui/ ← dumb, specifice domeniului
│ ├── data-access/ ← api + store
│ └── models/ ← tipuri, DTO-uri, mapperi
├── cart/
└── checkout/Rădăcina aplicației. Aici stau doar fișierele de pornire și cele trei zone: core, shared, features.
Cele trei zone, într-o frază
| Zonă | Întrebarea de test | Poate importa din |
|---|---|---|
core/ | „E folosit o dată, la nivel de aplicație, sau e infrastructură?” | shared |
shared/ | „Ar funcționa identic copiat în alt proiect?” | doar alt shared |
features/x/ | „E despre domeniul X?” | shared, core (servicii) |
Feature-ul pe dinăuntru
În interiorul unui feature, subfolderele urmează rolurile din Smart vs Dumb: pages (smart, rutate), ui (dumb), data-access (api + store) și models (tipuri). Nu le crea de la început — un feature mic stă bine plat:
// Feature mic? Fără subfoldere — plat, până devine incomod (~6–8 fișiere)
features/settings/
├── settings.routes.ts
├── settings-page.ts
├── settings-page.html
├── settings-store.ts
├── notification-toggle.ts
└── settings.ts ← tipurileLazy loading pe feature
// Fiecare feature e lazy: un singur punct de intrare (fișierul de rute)
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: foldere pe tip
// ✗ Organizare „pe tip” — scalează prost
src/app/
├── components/ (120 de fișiere amestecate din toate domeniile)
├── services/ (40)
├── models/ (60)
└── pipes/
// Ca să modifici „checkout” deschizi 4 foldere și cauți printre sute de fișiere.
// Nu poți face lazy loading curat, nu poți impune granițe între domenii.Pentru monorepo-uri mari
// Monorepo mare (Nx): aceleași idei, dar ca librării cu tag-uri și reguli verificate de lint
libs/
├── products/
│ ├── feature-list/ type:feature (pagini smart)
│ ├── 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/
Reguli tipice: feature → ui, data-access, util ✓ ui → util ✓ ui → data-access ✕Reguli de aur
- Colocare: ce se schimbă împreună stă împreună (componenta, template-ul, stilurile, testul, tipurile ei).
- Plat până doare: adaugi un subfolder când ai prea multe fișiere, nu „pentru viitor”.
- Feature-urile nu se importă între ele. Comunicare prin
core(store-uri globale),sharedsau prin URL. Vezi Granițe. - Fiecare feature are un singur punct de intrare: fișierul de rute.
- Numele folderului = numele domeniului, la plural sau singular — dar consecvent.
Capcane frecvente
sharedca „ladă de vechituri”: tot ce nu știi unde să pui ajunge acolo. Vezi Shared vs feature pentru reguli clare.corecu componente refolosite în pagini:coree pentru lucruri folosite o dată (shell, header), nu pentru UI comun.- Un singur feature „common” sau „general”: e un
shareddeghizat, cu domeniu amestecat.