Costruire un'app Angular che funziona è relativamente semplice. Costruire un'app Angular che rimane ordinata, leggibile, scalabile e facile da mantenere dopo mesi o anni di sviluppo è tutta un'altra cosa. La vera differenza tra un progetto amatoriale e un progetto professionale non sta solo nel codice che “fa il suo lavoro”, ma nell'architettura che permette al team di continuare a sviluppare senza creare caos.
Quando un'applicazione cresce, aumentano le pagine, i componenti, i servizi, le chiamate HTTP, gli stati da gestire, i permessi, le rotte, i form, le validazioni e le dipendenze tra le varie parti del sistema. Se tutto viene messo in cartelle generiche come components, services e models, dopo poco tempo diventa difficile capire dove si trova una funzionalità, chi usa cosa e quali parti del codice possono essere modificate senza rompere il resto.
In questa guida vediamo come strutturare un'app Angular moderna usando una feature-based architecture, una separazione chiara tra core e shared, i standalone components, i signals, il pattern smart vs dumb components e una folder structure pensata per progetti reali.
Perché l'architettura è importante in Angular
Angular è un framework molto potente perché offre già una struttura chiara: componenti, servizi, dependency injection, routing, form, HTTP client, guard, interceptor e molto altro. Tuttavia, proprio perché Angular offre tanti strumenti, è facile usarli senza una strategia coerente.
Un progetto piccolo può sopravvivere anche con una struttura poco ordinata. Un progetto medio o grande, invece, ha bisogno di regole. Senza regole, ogni sviluppatore organizza il codice a modo suo, i componenti diventano troppo grandi, i servizi accumulano responsabilità non correlate e il progetto si trasforma lentamente in un blocco difficile da modificare.
Una buona architettura Angular deve aiutare a raggiungere alcuni obiettivi fondamentali:
- Scalabilità: aggiungere nuove funzionalità senza dover riorganizzare tutto il progetto.
- Manutenibilità: capire rapidamente dove si trova il codice e come modificarlo.
- Separazione delle responsabilità: ogni file, componente o servizio deve avere un ruolo chiaro.
- Testabilità: il codice deve essere facile da testare in modo isolato.
- Performance: la struttura deve favorire lazy loading, bundle più piccoli e rendering efficiente.
- Collaborazione: più sviluppatori devono poter lavorare sulla stessa app senza pestarsi i piedi.
Il punto centrale è questo: l'architettura non serve a complicare il progetto, ma a renderlo più prevedibile. Quando una struttura è prevedibile, ogni nuova feature ha un posto naturale in cui vivere.
Feature-based architecture: organizzare il codice per funzionalità
Uno degli errori più comuni nei progetti Angular è organizzare il codice per tipo tecnico invece che per dominio funzionale. Una struttura di questo tipo può sembrare ordinata all'inizio:
src/app/
components/
services/
models/
pipes/
directives/
pages/
Il problema è che questa organizzazione non racconta nulla sul prodotto. Se stai lavorando alla sezione progetti, dovrai cercare i componenti in components, i servizi in services, i modelli in models, le pagine in pages e così via. Ogni feature è sparsa in tutto il progetto.
Una feature-based architecture, invece, organizza il codice intorno alle funzionalità dell'applicazione. Ad esempio:
src/app/
features/
dashboard/
blog/
projects/
experiences/
auth/
admin/
Ogni cartella rappresenta una parte reale del prodotto. Tutto ciò che riguarda il blog sta dentro features/blog. Tutto ciò che riguarda i progetti sta dentro features/projects. Tutto ciò che riguarda l'admin sta dentro features/admin.
Questo approccio migliora enormemente la leggibilità del progetto. Quando devi modificare una funzionalità, sai dove andare. Quando devi eliminare una feature, sai quali file sono coinvolti. Quando un nuovo sviluppatore entra nel team, può capire l'app partendo dalle funzionalità e non da cartelle tecniche generiche.
Una feature deve essere il più possibile autonoma
Una buona feature Angular dovrebbe contenere tutto ciò che le serve per funzionare: pagine, componenti specifici, servizi di accesso ai dati, modelli, store locali, route e utility interne.
Esempio di struttura per una feature projects:
src/app/features/projects/
projects.routes.ts
pages/
projects-list-page.component.ts
project-detail-page.component.ts
components/
project-card.component.ts
project-filters.component.ts
project-empty-state.component.ts
data-access/
projects-api.service.ts
projects-store.service.ts
models/
project.model.ts
project-filter.model.ts
utils/
project-status.util.ts
Questa struttura rende la feature indipendente e facile da capire. Le pagine sono separate dai componenti più piccoli, la logica di accesso ai dati è in data-access, i tipi TypeScript sono in models e le funzioni di supporto specifiche della feature sono in utils.
Regola importante: evitare dipendenze tra feature
Una feature non dovrebbe importare direttamente componenti, servizi o modelli da un'altra feature. Ad esempio, features/blog non dovrebbe importare codice da features/projects. Questo crea accoppiamento e rende difficile modificare una feature senza impattare le altre.
Se due feature hanno bisogno dello stesso componente o della stessa utility, probabilmente quell'elemento deve essere spostato in shared. Se invece condividono una logica di dominio importante, può essere utile creare una cartella o libreria dedicata, ma sempre con confini chiari.
Core e Shared: differenza fondamentale
In molti progetti Angular troviamo le cartelle core e shared, ma spesso vengono usate male. Capire la differenza tra queste due aree è essenziale per mantenere il progetto pulito.
Core: ciò che appartiene all'applicazione globale
La cartella core contiene codice globale, usato a livello applicativo, spesso inizializzato una sola volta. Qui troviamo elementi come autenticazione, interceptor HTTP, guard globali, layout principale, gestione errori, configurazioni, servizi singleton e logica infrastrutturale.
Esempio:
src/app/core/
auth/
auth.service.ts
auth.guard.ts
auth.interceptor.ts
http/
api-error.interceptor.ts
http-context.tokens.ts
layout/
main-layout.component.ts
admin-layout.component.ts
config/
app-config.token.ts
guards/
role.guard.ts
services/
logger.service.ts
storage.service.ts
Il core non dovrebbe diventare una discarica di servizi casuali. Deve contenere solo ciò che è davvero globale. Se un servizio serve solo alla feature blog, non va in core: va dentro features/blog/data-access.
Shared: ciò che è riutilizzabile e privo di logica di business specifica
La cartella shared contiene elementi riutilizzabili in più parti dell'applicazione, ma non legati a una feature specifica. Qui troviamo componenti UI generici, pipe, directive, helper e modelli veramente condivisi.
src/app/shared/
ui/
button/
modal/
card/
badge/
spinner/
pipes/
truncate.pipe.ts
safe-html.pipe.ts
directives/
autofocus.directive.ts
click-outside.directive.ts
utils/
date-format.util.ts
string.util.ts
models/
pagination.model.ts
api-response.model.ts
Un componente come app-button, app-modal o app-spinner può stare in shared/ui. Un componente come project-card, invece, non dovrebbe stare in shared, perché appartiene al dominio dei progetti.
Errore comune: mettere tutto in Shared
Uno degli anti-pattern più frequenti è creare una cartella shared enorme che contiene qualsiasi cosa. All'inizio sembra comodo, ma dopo qualche mese diventa impossibile capire quali componenti sono davvero generici e quali sono stati messi lì solo per comodità.
La regola pratica è semplice: se un componente contiene parole, logica o concetti legati a una feature specifica, non è shared. Se invece è generico, riutilizzabile e indipendente dal dominio, può andare in shared.
Standalone components: Angular moderno senza NgModule inutili
Con Angular moderno, i standalone components sono diventati il modo consigliato per costruire applicazioni più semplici, esplicite e modulari. In passato, ogni componente doveva essere dichiarato dentro un NgModule. Questo spesso portava alla creazione di moduli molto grandi o poco chiari.
Con i componenti standalone, ogni componente dichiara direttamente le proprie dipendenze attraverso la proprietà imports. Questo rende il codice più leggibile: guardando un componente puoi capire subito quali altri componenti, directive o pipe utilizza.
@Component({
selector: 'app-project-card',
standalone: true,
imports: [DatePipe],
template: `
<article class="project-card">
<h3>{{ project().title }}</h3>
<p>{{ project().description }}</p>
<small>Pubblicato il {{ project().createdAt | date }}</small>
<button type="button" (click)="open.emit(project().id)">
Apri progetto
</button>
</article>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectCardComponent {
project = input.required<Project>();
open = output<string>();
}
Questo approccio ha diversi vantaggi:
- le dipendenze sono esplicite;
- i componenti sono più facili da spostare e testare;
- il lazy loading diventa più naturale;
- si riduce la necessità di moduli intermedi;
- il progetto risulta più leggero mentalmente.
Bootstrap dell'app con standalone
In un'app Angular moderna, il bootstrap può essere gestito senza un AppModule tradizionale. La configurazione globale viene spesso definita in app.config.ts.
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(withInterceptors([
authInterceptor,
apiErrorInterceptor
]))
]
};
E nel file main.ts:
bootstrapApplication(AppComponent, appConfig)
.catch((error) => console.error(error));
Questa configurazione separa chiaramente il punto di avvio dell'app dalla logica delle feature.
Routing e lazy loading per feature
Il routing è una parte centrale dell'architettura Angular. In un progetto scalabile, ogni feature dovrebbe avere le proprie route, caricate in lazy loading quando possibile.
Nel file principale app.routes.ts possiamo definire solo le rotte principali:
export const routes: Routes = [
{
path: '',
loadComponent: () =>
import('./features/home/pages/home-page.component')
.then((m) => m.HomePageComponent)
},
{
path: 'projects',
loadChildren: () =>
import('./features/projects/projects.routes')
.then((m) => m.PROJECTS_ROUTES)
},
{
path: 'blog',
loadChildren: () =>
import('./features/blog/blog.routes')
.then((m) => m.BLOG_ROUTES)
},
{
path: 'admin',
canMatch: [authGuard],
loadChildren: () =>
import('./features/admin/admin.routes')
.then((m) => m.ADMIN_ROUTES)
}
];
Dentro la feature projects, invece, definiamo le route specifiche:
export const PROJECTS_ROUTES: Routes = [
{
path: '',
loadComponent: () =>
import('./pages/projects-list-page.component')
.then((m) => m.ProjectsListPageComponent)
},
{
path: ':id',
loadComponent: () =>
import('./pages/project-detail-page.component')
.then((m) => m.ProjectDetailPageComponent)
}
];
Questo approccio mantiene il file principale delle rotte pulito e permette ad Angular di caricare solo il codice necessario per la sezione visitata dall'utente.
Signals: gestione dello stato più semplice e reattiva
I signals sono uno degli strumenti più importanti nell'Angular moderno. Permettono di gestire stato locale e derivato in modo più semplice, leggibile e performante rispetto a molte soluzioni tradizionali.
Un signal rappresenta un valore reattivo. Quando il valore cambia, Angular sa quali parti della UI devono essere aggiornate. Questo rende lo stato più esplicito e riduce la complessità in molti scenari.
Esempio semplice:
const count = signal(0);
const double = computed(() => count() * 2);
function increment(): void {
count.update((value) => value + 1);
}
In un'app scalabile, i signals possono essere usati a diversi livelli:
- stato locale del componente, come tab attiva, filtri, apertura di una modale;
- stato derivato, come totale elementi, elementi filtrati, stato vuoto;
- store di feature, quando una sezione dell'app ha dati condivisi tra più componenti;
- facade, per esporre alla UI uno stato semplice e già pronto.
Esempio di store con signals
Una buona pratica consiste nel creare uno store specifico per la feature, evitando di mettere tutta la logica dentro i componenti.
@Injectable()
export class ProjectsStore {
private readonly api = inject(ProjectsApiService);
private readonly _projects = signal<Project[]>([]);
private readonly _loading = signal(false);
private readonly _error = signal<string | null>(null);
private readonly _query = signal('');
readonly projects = this._projects.asReadonly();
readonly loading = this._loading.asReadonly();
readonly error = this._error.asReadonly();
readonly query = this._query.asReadonly();
readonly filteredProjects = computed(() => {
const query = this._query().toLowerCase().trim();
if (!query) {
return this._projects();
}
return this._projects().filter((project) =>
project.title.toLowerCase().includes(query)
);
});
readonly total = computed(() => this.filteredProjects().length);
async loadProjects(): Promise<void> {
this._loading.set(true);
this._error.set(null);
try {
const projects = await firstValueFrom(this.api.getProjects());
this._projects.set(projects);
} catch {
this._error.set('Impossibile caricare i progetti.');
} finally {
this._loading.set(false);
}
}
setQuery(query: string): void {
this._query.set(query);
}
}
Questo store ha una responsabilità chiara: gestire lo stato della feature progetti. Il componente non deve sapere come vengono caricati i dati o come vengono filtrati. Deve solo leggere signals e chiamare metodi esposti dallo store.
Dove fornire uno store di feature
Se lo store serve solo a una specifica feature, può essere fornito a livello di route o di componente. In questo modo il suo ciclo di vita è legato alla feature stessa e non rimane globale inutilmente.
export const PROJECTS_ROUTES: Routes = [
{
path: '',
providers: [ProjectsStore, ProjectsApiService],
loadComponent: () =>
import('./pages/projects-list-page.component')
.then((m) => m.ProjectsListPageComponent)
}
];
Questa scelta evita di riempire il root injector con servizi che non devono vivere per tutta la durata dell'applicazione.
Signals e RxJS: non sono nemici
Un errore comune è pensare che i signals sostituiscano completamente RxJS. In realtà, i due strumenti possono convivere benissimo. I signals sono ottimi per stato sincrono, derivazioni e UI state. RxJS rimane molto utile per flussi asincroni, eventi complessi, WebSocket, debounce, retry, combinazioni di stream e cancellazione automatica di richieste.
Una regola pratica utile:
- usa signals per rappresentare lo stato corrente della UI;
- usa computed per valori derivati;
- usa RxJS quando devi modellare flussi asincroni nel tempo;
- usa conversioni come
toSignalquando vuoi esporre un Observable alla template in modo più semplice.
Smart vs Dumb Components
Il pattern smart vs dumb components è uno dei più utili per mantenere un'app Angular pulita. L'idea è separare i componenti che gestiscono logica, dati e comunicazione da quelli che si occupano solo di visualizzare informazioni.
Smart components
Gli smart components, chiamati anche container components, sono componenti consapevoli della feature. Possono usare servizi, store, router, parametri di rotta e logica applicativa. Di solito corrispondono a una pagina o a un componente principale della feature.
Esempio di smart component:
@Component({
selector: 'app-projects-list-page',
standalone: true,
imports: [
ProjectCardComponent,
ProjectFiltersComponent,
SpinnerComponent
],
template: `
<section>
<h2>Progetti</h2>
<app-project-filters
[query]="store.query()"
(queryChange)="store.setQuery($event)"
/>
@if (store.loading()) {
<app-spinner />
} @else if (store.error()) {
<p class="error">{{ store.error() }}</p>
} @else {
<div class="projects-grid">
@for (project of store.filteredProjects(); track project.id) {
<app-project-card
[project]="project"
(open)="openProject($event)"
/>
}
</div>
}
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectsListPageComponent implements OnInit {
readonly store = inject(ProjectsStore);
private readonly router = inject(Router);
ngOnInit(): void {
this.store.loadProjects();
}
openProject(id: string): void {
this.router.navigate(['/projects', id]);
}
}
Questo componente è smart perché coordina la pagina: carica i dati, legge lo store, gestisce la navigazione e passa informazioni ai componenti figli.
Dumb components
I dumb components, chiamati anche presentational components, sono componenti semplici, riutilizzabili e facili da testare. Ricevono dati tramite input ed emettono eventi tramite output. Non dovrebbero conoscere router, servizi HTTP o store globali.
@Component({
selector: 'app-project-filters',
standalone: true,
template: `
<label>
Cerca progetto
<input
type="search"
[value]="query()"
(input)="onInput($event)"
placeholder="Cerca per titolo..."
/>
</label>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ProjectFiltersComponent {
query = input('');
queryChange = output<string>();
onInput(event: Event): void {
const target = event.target as HTMLInputElement;
this.queryChange.emit(target.value);
}
}
Questo componente non sa nulla della feature completa. Non sa da dove arrivano i progetti, non conosce l'API, non naviga, non modifica direttamente lo store. Il suo compito è solo mostrare un input e comunicare il nuovo valore.
Perché questa separazione funziona
Separare smart e dumb components porta vantaggi concreti:
- i componenti presentazionali sono più facili da riutilizzare;
- i test diventano più semplici;
- la logica applicativa resta nei container o negli store;
- la UI diventa più prevedibile;
- si riduce il rischio di componenti enormi e difficili da mantenere.
Non bisogna però applicare il pattern in modo dogmatico. Per componenti molto piccoli o feature semplici, può essere sufficiente un singolo componente. L'obiettivo non è creare più file possibile, ma mantenere responsabilità chiare.
Folder structure consigliata per un'app Angular scalabile
Una struttura solida per un'app Angular moderna può essere questa:
src/
app/
app.component.ts
app.config.ts
app.routes.ts
core/
auth/
auth.service.ts
auth.guard.ts
auth.interceptor.ts
http/
api-error.interceptor.ts
layout/
main-layout.component.ts
admin-layout.component.ts
services/
logger.service.ts
storage.service.ts
shared/
ui/
button/
button.component.ts
modal/
modal.component.ts
spinner/
spinner.component.ts
pipes/
truncate.pipe.ts
directives/
autofocus.directive.ts
models/
pagination.model.ts
api-response.model.ts
utils/
date.util.ts
features/
home/
pages/
home-page.component.ts
projects/
projects.routes.ts
pages/
projects-list-page.component.ts
project-detail-page.component.ts
components/
project-card.component.ts
project-filters.component.ts
data-access/
projects-api.service.ts
projects-store.service.ts
models/
project.model.ts
utils/
project-status.util.ts
blog/
blog.routes.ts
pages/
blog-list-page.component.ts
blog-detail-page.component.ts
components/
blog-card.component.ts
blog-search.component.ts
data-access/
blog-api.service.ts
blog-store.service.ts
models/
blog-post.model.ts
admin/
admin.routes.ts
pages/
admin-dashboard-page.component.ts
edit-post-page.component.ts
components/
admin-sidebar.component.ts
content-editor.component.ts
data-access/
admin-api.service.ts
admin-store.service.ts
models/
admin-user.model.ts
environments/
environment.ts
Questa struttura è abbastanza semplice per essere capita rapidamente, ma abbastanza solida per crescere nel tempo.
Spiegazione delle cartelle principali
app.config.ts contiene i provider globali: router, HTTP client, interceptor, configurazioni globali e servizi applicativi.
app.routes.ts contiene solo le rotte principali e delega le rotte dettagliate alle singole feature.
core contiene ciò che vive a livello globale: auth, interceptor, layout, logger, configurazioni e servizi infrastrutturali.
shared contiene elementi generici e riutilizzabili, senza logica di business specifica.
features contiene il cuore dell'applicazione, organizzato per dominio funzionale.
pages contiene componenti collegati direttamente alle route. Di solito sono smart components.
components contiene componenti specifici della feature, spesso dumb o semi-presentational.
data-access contiene servizi API, store, facade e logica di accesso ai dati.
models contiene interfacce e tipi TypeScript legati alla feature.
utils contiene funzioni pure specifiche della feature.
Data-access layer: isolare API e stato
In un'app Angular scalabile, i componenti non dovrebbero parlare direttamente con HttpClient. Se ogni componente fa chiamate HTTP autonome, la logica si duplica e diventa difficile gestire loading, errori, caching e trasformazione dei dati.
Meglio creare un livello data-access per ogni feature. Questo livello può contenere:
- servizi API;
- store basati su signals;
- facade;
- mapper tra DTO e modelli UI;
- logica di caching locale della feature.
Esempio di API service:
@Injectable()
export class ProjectsApiService {
private readonly http = inject(HttpClient);
private readonly baseUrl = '/api/projects';
getProjects(): Observable<Project[]> {
return this.http.get<Project[]>(this.baseUrl);
}
getProjectById(id: string): Observable<Project> {
return this.http.get<Project>(`${this.baseUrl}/${id}`);
}
}
Il componente non deve conoscere endpoint, URL o dettagli HTTP. Deve comunicare con lo store o con una facade. Questo mantiene la UI pulita e facilita i test.
Regole pratiche di dipendenza
Per evitare caos architetturale, è utile stabilire regole chiare sulle dipendenze:
- Una feature può importare da shared, perché shared contiene elementi generici.
- Una feature può usare servizi core, come auth o logger, se davvero necessari.
- Shared non deve importare feature, altrimenti smette di essere generico.
- Shared dovrebbe evitare dipendenze forti da core, per restare riutilizzabile.
- Una feature non dovrebbe importare direttamente un'altra feature.
- Core non dovrebbe contenere logica specifica di una singola feature.
Una possibile direzione delle dipendenze è questa:
features ---> shared
features ---> core
core ---> shared
shared ---> nessuna feature
Più queste regole sono rispettate, più l'app rimane modulare.
Naming convention consigliate
Le convenzioni di naming sembrano dettagli, ma nei progetti grandi fanno una grande differenza. Un nome coerente riduce il tempo necessario per capire il ruolo di un file.
*.page.tsper componenti collegati a una route.*.component.tsper componenti UI normali.*.service.tsper servizi generici.*.api.service.tsper servizi che comunicano con backend.*.store.tso*.store.service.tsper stato di feature.*.guard.tsper route guard.*.interceptor.tsper interceptor HTTP.*.model.tsper interfacce e tipi principali.*.util.tsper funzioni pure di supporto.
Ad esempio, project-detail-page.component.ts comunica subito che si tratta di una pagina. projects-api.service.ts comunica che quel servizio parla con il backend. projects-store.service.ts comunica che quel file gestisce lo stato della feature.
Performance e architettura
Una buona architettura Angular non riguarda solo l'ordine dei file. Influenza anche le performance dell'applicazione.
Usare feature lazy-loaded significa evitare di caricare tutto il codice all'avvio. Se un utente visita solo la home page, non ha senso scaricare immediatamente anche il codice dell'admin, dell'editor, della dashboard e di tutte le sezioni interne.
I standalone components aiutano perché rendono più semplice caricare componenti e route in modo granulare. I signals aiutano perché rendono il rendering più preciso e lo stato più facile da tracciare. Il pattern smart/dumb aiuta perché riduce componenti enormi e template difficili da ottimizzare.
Alcune buone pratiche:
- usa lazy loading per feature non immediatamente necessarie;
- usa
ChangeDetectionStrategy.OnPushnei componenti; - usa
@forcontrackper liste performanti; - mantieni i componenti piccoli e focalizzati;
- evita logica pesante direttamente nel template;
- usa signals e computed per valori derivati;
- carica immagini e asset in modo ottimizzato;
- separa l'admin dal frontend pubblico quando possibile.
Errori comuni da evitare
1. Componenti troppo grandi
Un componente che contiene template enorme, chiamate HTTP, gestione form, stato, validazioni, mapping dati e logica di navigazione diventa rapidamente ingestibile. Se un componente supera troppe responsabilità, è il momento di dividerlo.
2. Services globali per tutto
Non tutti i servizi devono essere providedIn: 'root'. Se un servizio serve solo a una feature, fornirlo a livello di route o componente può essere più corretto. Questo riduce stato globale non necessario e rende il ciclo di vita più prevedibile.
3. Shared troppo grande
Una cartella shared enorme è spesso segnale di architettura debole. Shared deve contenere componenti e utility davvero generici, non pezzi di feature messi lì per comodità.
4. Feature accoppiate tra loro
Quando una feature importa direttamente file da un'altra feature, il progetto diventa fragile. Meglio estrarre il codice condiviso in un'area comune con responsabilità chiara.
5. Logica di business nel template
Il template deve restare leggibile. Se una condizione diventa troppo complessa, spostala in un computed, in un metodo chiaro o nello store della feature.
6. Mancanza di convenzioni
Se ogni sviluppatore crea cartelle e nomi a modo suo, il progetto perde coerenza. Le convenzioni devono essere semplici, documentate e rispettate.
Esempio pratico: una feature Blog ben strutturata
Immaginiamo una sezione blog con lista articoli, dettaglio articolo, ricerca, filtri per categoria e gestione contenuti lato admin. Una struttura ordinata potrebbe essere:
src/app/features/blog/
blog.routes.ts
pages/
blog-list-page.component.ts
blog-detail-page.component.ts
components/
blog-card.component.ts
blog-search.component.ts
blog-category-filter.component.ts
blog-empty-state.component.ts
data-access/
blog-api.service.ts
blog-store.service.ts
models/
blog-post.model.ts
blog-category.model.ts
utils/
reading-time.util.ts
slug.util.ts
La pagina blog-list-page è smart: usa lo store, carica i dati, gestisce filtri e ricerca. Il componente blog-card è dumb: riceve un articolo e mostra titolo, excerpt, immagine e data. Il servizio blog-api parla con il backend. Lo store mantiene stato, loading, errori e articoli filtrati.
Questo tipo di separazione permette di modificare il layout delle card senza toccare la logica dati, oppure cambiare endpoint API senza modificare i componenti presentazionali.
Checklist finale per un'architettura Angular scalabile
Prima di considerare solida la struttura della tua app Angular, puoi usare questa checklist:
- Le funzionalità principali sono organizzate dentro
features? - Ogni feature ha route, pagine, componenti, data-access e modelli separati?
- Il codice globale è davvero limitato a
core? sharedcontiene solo elementi generici e riutilizzabili?- Le feature evitano dipendenze dirette tra loro?
- Le route principali usano lazy loading?
- I componenti standalone dichiarano chiaramente le proprie dipendenze?
- La logica HTTP è fuori dai componenti?
- Lo stato della feature è gestito da store, facade o servizi dedicati?
- I signals sono usati per stato locale e valori derivati?
- RxJS viene usato dove serve davvero modellare flussi asincroni?
- I componenti smart e dumb hanno responsabilità separate?
- I nomi dei file sono coerenti?
- I componenti sono piccoli, leggibili e testabili?
- Esiste una convenzione condivisa dal team?
Conclusione
Organizzare bene un progetto Angular non significa creare una struttura complicata. Significa creare una struttura chiara, prevedibile e adatta alla crescita. La feature-based architecture aiuta a ragionare per funzionalità reali. La separazione tra core e shared evita confusione. I standalone components rendono le dipendenze più esplicite. I signals semplificano la gestione dello stato. Il pattern smart vs dumb components migliora leggibilità, riuso e testabilità.
La cosa più importante è non aspettare che il progetto diventi grande per pensare all'architettura. Le decisioni prese all'inizio influenzano tutto il ciclo di vita dell'applicazione. Una struttura pulita permette di aggiungere nuove pagine, nuove feature e nuovi sviluppatori senza trasformare il codice in un labirinto.
Se stai costruendo un'app Angular professionale, parti da una regola semplice: ogni cosa deve avere un posto preciso. Le feature devono contenere la logica del prodotto, core deve contenere l'infrastruttura globale, shared deve contenere elementi veramente riutilizzabili. Da lì, aggiungi standalone components, lazy loading, signals e una separazione chiara tra componenti smart e dumb.
Un'architettura scalabile non è quella più complessa, ma quella che il team riesce a capire, mantenere ed evolvere nel tempo. Angular offre tutti gli strumenti necessari: sta a noi usarli con disciplina, coerenza e buon senso.