O structură de foldere bună nu ajută dacă oricine poate importa orice de oriunde. Granițele sunt regulile despre cine are voie să depindă de cine. Ele fac aplicația ușor de schimbat: când știi că shared nu depinde de niciun feature, știi că îl poți modifica fără să spargi domeniul.
Matricea dependențelor
| importă din → fișier din ↓ | app (config, rute) | core | shared | features/products | features/cart |
|---|---|---|---|---|---|
| app (config, rute) | |||||
| core | |||||
| shared | |||||
| features/products | |||||
| features/cart |
features → core, shared · core → shared · shared → nimic altceva. Niciodată invers, și niciodată feature → feature direct.
Path aliases
{
"compilerOptions": {
"paths": {
"@core/*": ["./src/app/core/*"],
"@shared/*": ["./src/app/shared/*"],
"@features/*": ["./src/app/features/*"]
}
}
}
// (exact configurația folosită de acest site)Barrel files (index.ts)
// features/users/index.ts — API-ul PUBLIC al feature-ului (singurul barrel)
export { UserAvatar } from './ui/user-avatar';
export { UsersStore } from './data-access/users-store';
export type { User, Role } from './models/user';
// shared/ui/index.ts — util pentru a importa mai multe componente UI deodată
export { Button } from './button';
export { Card } from './card';
export const PAGE_KIT = [Button, Card] as const;Verificare automată
Regulile scrise doar în documentație se încalcă în prima săptămână. Pune-le în lint:
// eslint.config.mjs — reguli de graniță verificate automat
export default [
{
files: ['src/app/shared/**/*.ts'],
rules: {
'no-restricted-imports': ['error', {
patterns: [
{ group: ['@features/*'], message: 'shared nu are voie să depindă de feature-uri.' },
{ group: ['@core/*'], message: 'shared nu depinde de core — primește datele prin input-uri.' },
],
}],
},
},
{
files: ['src/app/features/**/*.ts'],
rules: {
'no-restricted-imports': ['error', {
patterns: [
{ group: ['@features/*/*'], message: 'Importă alt feature doar prin API-ul lui public (@features/x).' },
],
}],
},
},
];
// Alternative: eslint-plugin-boundaries, Nx @nx/enforce-module-boundaries (cu tag-uri)Cum comunică două feature-uri fără să se importe?
- Prin URL: produsul trimite utilizatorul la
/cart?add=42. - Prin core: un store global (
CartStoreîncore) pe care îl folosesc amândouă. - Prin shared: un tip sau o componentă generică comună.
- Prin API public: dacă A chiar are nevoie de o componentă din B, B o exportă explicit în
index.ts.
Dependențe circulare: cum le rupi
Un ciclu apare când A importă din B și B, direct sau prin alții, importă din A. Simptomele sunt ciudate: o clasă e undefined la pornire, testele trec doar în altă ordine, build-ul încetinește. Trei ieșiri, în ordinea în care merită încercate:
- Coboară partea comună. Dacă amândouă au nevoie de același tip sau de aceeași funcție, mut-o în
sharedsau încore, iar ciclul dispare. - Inversează dependența. Cel de jos declară un contract (un
InjectionTokensau o clasă abstractă), iar cel de sus îl implementează și îl furnizează. Cel de jos nu mai importă nimic de sus. - Comunică fără import. Prin URL, printr-un store din
coresau printr-un eveniment, ca în secțiunea de mai sus.
// Înainte: core/notifier importa din features/orders pentru link → ciclu. După: core declară un token.
// core/notifications/notifier.ts
export const ORDER_LINK = new InjectionToken<(id: string) => string>('ORDER_LINK');
@Service()
export class Notifier {
private readonly link = inject(ORDER_LINK); // nu mai importă nimic din features
orderShipped(id: string) {
this.show(`Comanda a fost expediată`, this.link(id));
}
}
// features/orders/orders.providers.ts — feature-ul furnizează implementarea
export const provideOrders = () => [
{ provide: ORDER_LINK, useValue: (id: string) => `/orders/${id}` },
];Granițe și pentru librăriile externe
Aceeași idee se aplică și pachetelor din node_modules. Dacă 40 de componente importă direct o librărie de grafice sau de date, schimbarea ei înseamnă 40 de fișiere modificate. Pune librăria în spatele unui adaptor mic, al tău, și interzice importul direct în lint.
// shared/charts/line-chart.ts — singurul fișier care importă librăria
import { Chart } from 'some-chart-lib';
@Component({ selector: 'app-line-chart', template: '<canvas #c></canvas>' })
export class LineChart {
readonly points = input.required<readonly { x: string; y: number }[]>();
// restul aplicației vede doar <app-line-chart [points]="...">
}Ca să impui aceleași reguli între librării, vezi Monorepo, librării & granițe; zonele în sine sunt descrise la Shared vs feature.
Capcane frecvente
- Barrel în fiecare folder → dependențe circulare și build-uri lente.
- Alias care ascunde încălcări:
@features/cart/data-access/cart-storedintr-un alt feature e tot o încălcare — de aceea regula de lint. - „Doar de data asta”: o singură excepție devine pattern. Mută codul în locul corect.
Fișă rapidă
features → core, shared permis
core → shared
shared → nimic din aplicație
feature → feature interzis direct: prin URL, store în core sau API public
ciclu? 1) coboară partea comună 2) inversează cu un token 3) comunică fără import
librărie externă? un adaptor în shared + no-restricted-imports
barrel: unul singur, la rădăcina feature-ului