Negli ultimi tempi mi sto occupando della migrazione di un’applicazione di livello enterprise, il progetto sarà lungo e richiederà sicuramente degli anni di sviluppo. Nello specifico mi sto occupando del frontend e sto sviluppando l’architettura base per poter poi sviluppare il resto dell’applicazione. L’applicazione in questione è molto complessa sia per quanto riguarda il numero di pagine (circa 3000 😱), sia per quanto riguarda le personalizzazioni fatte negli anni per i vari clienti.
In uno scenario così complesso costruire delle solide fondamenta per applicazione è condizione necessaria per la buona riuscita del progetto.
Per quanto riguarda il frontend è stato scelto di utilizare Angular, framework con alle spalle molti anni di esperienza e che con le ultime novità sta riguadagnando il primo posto come uno dei migliori framework web disponibili al momento.
Nella mia esperienza ho realizzato diverse applicazioni complesse ma mai di questa portata, dunque come organizzare il progetto nel migliore dei modi? Il mio sguardo, la mia mente e il mio cuore si sono orientati senza indugio verso NX, uno strumento nato proprio per organizzare progetti complessi. Lo scopo di questo articolo non è quello di approfondire ogni aspetto (anche perchè servirebbe un libro!) ma piuttosto quello di introdurlo a chi non l’ha mai sentito nominare.
Cos’è Nx e quando conviene usarlo
Nx è un set di strumenti di sviluppo che permette di gestire workspace monorepo per applicazioni JavaScript/TypeScript (nel mio caso Angular). Creato da Nrwl, Nx consente di mantenere più progetti correlati all’interno di un singolo repository, facilitando la condivisione di codice e garantendo coerenza in tutta la codebase. A differenza di un classico progetto frontend (Angular, React,..) dove tutto è all’interno di un singolo progetto, in un monorepo possiamo avere il codice suddiviso in più librerie e più webapp (separate oppure correlate tra di loro, in questo caso parliamo di microfrontend). Quali sono i vantaggi?
- Condivisione: librerie e componenti possono essere facilmente condivisi tra applicazioni senza la complessità di gestire pacchetti npm separati.
- Refactoring atomici: le modifiche che interessano più progetti possono essere gestite in un unico commit
- Visibilità completa: i developer hanno una visione d’insieme dell’ecosistema applicativo
- Gestione coerente delle dipendenze: un’unica versione delle dipendenze per tutti i progetti
- CI/CD ottimizzato: costruzione e test intelligenti solo dei progetti interessati dalle modifiche
Nx inoltre aggiunge strumenti specifici per rendere la gestione del monorepo efficiente e scalabile, come cache intelligente, l’analisi delle dipendenze e le build selettive (su questi aspetti ci soffermiamo più tardi)
Sembra incredibile, non è vero? Ma è sempre conveniente utilizzarlo? Non sempre, a volte sarebbe come sparare con un cannone ad una formica, ecco qualche esempio su quanto conviene oppure no:
Quando usare Nx:
- Per progetti di medie e grandi dimensioni con più applicazioni correlate
- Quando hai bisogno di condividere codice tra diverse applicazioni (divisione in librerie o applicazioni con parti in comuni)
- In team di sviluppo con più persone che lavorano su componenti diversi (un team per ogni libreria / set di librerie)
- Quando desideri un’infrastruttura di testing e build coerente
- Per standardizzare pratiche di sviluppo attraverso diversi team
- Quando il progetto deve crescere
Quando NON usarlo:
- Per piccole applicazioni standalone (vi ricordate della formica?)
- Quando la complessità aggiuntiva non è giustificata dai benefici
- In progetti con un ciclo di vita breve o prototipi rapidi
Angular classico vs. Nx: le differenze architetturali
Vediamo ora di soffermarci sulle principali differenze che ci sono, a livello di progetto, tra un progetto creato con la CLI di Angular o con Nx.
Struttura del workspace
Angular standard:
my-app/
├── src/
│ ├── app/
│ ├── assets/
│ └── ...
├── angular.json
├── package.json
└── ...
Nx workspace:
my-workspace/
├── apps/
│ ├── my-app/
│ │ ├── src/
│ │ └── ...
│ └── my-other-app/
├── libs/
│ ├── shared-ui/
│ ├── feature-auth/
│ └── ...
├── nx.json
├── workspace.json (o project.json)
├── package.json
└── ...
Già qui possiamo notare che la struttura del workspace di NX (si chiama così il contenitore di tutti i progetti), nasce con la possibilità di gestire progetti multipli. Potremmo avere delle librerie con dei moduli di dominio e diverse applicazioni che attingono da queste librerie, tutto all’interno di un unico workspace.
File di configurazione
In Nx, i principali file di configurazione sono:
nx.json
: Configura le opzioni globali del workspace Nxproject.json
: Sostituisce il tradizionaleangular.json
per ciascun progettotsconfig.base.json
: Contiene le configurazioni TypeScript di base per tutto il workspace. Ogni progetto avrà poi un suo file dedicato che estende questo file.
Nel workspace Nx, ogni applicazione e libreria ha il proprio file project.json
che definisce le sue configurazioni specifiche. Questo approccio modulare consente una gestione più granulare dei progetti.
Creazione progetto e comandi base
Iniziare con Nx è semplice ma bisogna sapere che ha un suo set di comandi. Così come con la CLI di Angular lanciamo il comando ng serve con NX dovremo utilizzare un suo comando dedicato. Vediamo ora i comandi principali per poter iniziare.
#Installazione globale di Nx CLI
npm install -g nx
# Creazione di un nuovo workspace
npx create-nx-workspace my-workspace-name
Durante il setup, ti verranno poste alcune domande sulla configurazione del tuo workspace e potrai scegliere tra vari preset, inclusi quelli per Angular.
Una volta creato il workspace, ecco alcuni comandi fondamentali:
# Generare una nuova applicazione Angular chiamata my-app-name
nx generate @nx/angular:application my-app-name
# Generare una libreria condivisa "shared-ui"
nx generate @nx/angular:library shared-ui
# Eseguire l'applicazione (corrisponde a ng serve)
nx serve my-app
# Eseguire i test
nx test my-app
# Eseguire il lint
nx lint my-app
# Costruire l'applicazione (corrisponde a ng build)
nx build my-app
La potenza della cache Nx
Uno dei (tanti) vantaggi di Nx è il suo sistema di cache. Quando esegui un comando come nx build
o nx test
, gli artefatti prodotti vengono memorizzati localmente. Se esegui nuovamente lo stesso comando senza modificare il codice, Nx utilizzerà i risultati memorizzati precedentemente invece di eseguire nuovamente l’operazione.
La cache funziona calcolando un hash basato su:
- Il codice sorgente del progetto
- Le dipendenze del progetto
- La configurazione del task
- L’ambiente di esecuzione
Per gestire la cache è possibile utilizzare questi comandi:
# Pulire la cache
nx reset
# Visualizzare lo stato della cache
nx report
Questo meccanismo di caching accelera notevolmente i cicli di sviluppo, soprattutto in progetti di grandi dimensioni o in ambienti CI/CD, evitando di ricompilare tutte quelle parti che non sono state modificate. Ulteriori potenzialità sono offerte dalla versione a pagamento di NX, chiamato NX Cloud.
Task in Nx: automazione a un nuovo livello
I task in Nx rappresentano le operazioni che puoi eseguire sui tuoi progetti, come build, test, lint, ecc. In un contesto dove potresti avere decine e decine di progetti, questo ti permette di definire dei comandi da lanciare in determinate situazioni, con il vantaggio che possono essere eseguiti anche in parallelo.
I task possono essere definiti in 3 differenti luoghi: package.json, project.json o nx.json
Ogni task è definito da:
- Un nome (come “build”)
- Un executor (il codice che esegue il task)
- Opzioni di configurazione
Puoi eseguire i task con nx run nome-progetto:nome-task
o con le scorciatoie come nx build project-name
.
Un aspetto potente dei task Nx è la possibilità di definire dipendenze tra di essi usando dependsOn
:
"build": {
"executor": "@nx/angular:webpack-browser",
"dependsOn": [
"^build"
],
"options": {...}
}
In questo esempio vien eseguito task “build” di tutti i progetti da cui dipende prima di costruire il progetto corrente.
I task potrebbero essere utilizzati per lanciare automaticamente i test a seguito di una compilazione, in modo automatico. Anche in questo caso entrano in gioco le cache viste poco fa, evitando di sprecare tempo nel fare i test a progetti che non sono cambiati!
Librerie in Nx: i mattoni del tuo monorepo
Come dice il titolo di questo paragrafo le librerie non sono altro che mattoni utilizzati per costruire la nostra applicazione. Progetti di grandi dimensioni prevedono la suddivisione del progetto in più parti, potendole poi utilizzare in più applicazioni e permettendo a team differenti di lavorare in parallelo su diversi contesti.
La documentazione di NX identifica alcune tipologie di librerie, questo non toglie che ogni team può decidere organizzazioni differenti:
- Feature Libraries: Contengono funzionalità specifiche di business, spesso legate a una particolare feature dell’applicazione. Ad esempio, una libreria per l’autenticazione o per la gestione degli ordini.
- UI Libraries: Contengono componenti UI riutilizzabili, come bottoni, form, o interi layout.
- Data-access Libraries: Gestiscono l’accesso ai dati, inclusi i servizi che comunicano con le API e la gestione degli stati dell’applicazione
- Utility Libraries: Contengono funzioni di utilità che possono essere utilizzate in diverse parti dell’applicazione.
Personalmente io apprezzo maggiormente la suddivisione delle librerie in:
- Core: contiene gli elementi di basso livello indispensabili per tutta l’applicazione, dalla gestione delle impostazioni, all’autenticazione. Qui non andiamo ad inserire elementi di UI.
- Shared: tutti gli elementi che possono essere utilizzati in più moduli, come ad esempio la UI (bottoni, componenti riutilizzabili,..)
- Features: una libreria dedicata per ogni contesto di dominio (anagrafiche, ordini, carrello,..)
Modalità di compilazione delle librerie
Oltre alla categorizzazione per funzionalità, Nx permette di creare 3 tipologie distinte di librerie:
- Simple : Sono il tipo predefinito quando si crea una libreria. Non hanno un target di build dedicato e vengono compilate insieme all’applicazione che le utilizza. Sono ideali per librerie di componenti UI, servizi condivisi e utility o in tutti quei casi in cui vogliamo suddividere i contesti di dominio senza doverli distribuire separatamente.
nx generate @nx/angular:library my-simple-lib
- Buildable: Possono essere compilate indipendentemente dalle applicazioni che le utilizzano. Hanno un proprio target di build e producono un output che può essere utilizzato da altre librerie o applicazioni. Questa modalità permette una migliore separazione e test più rapidi.
nx generate @nx/angular:library my-buildable-lib --buildable
- Publishable : Estendono le librerie buildable aggiungendo la possibilità di essere pubblicate su un registry npm. Sono adatte per codice che deve essere condiviso anche al di fuori del monorepo.
nx generate @nx/angular:library my-publishable-lib --publishable --importPath=@myorg/my-lib
La scelta del tipo di libreria dipende dalle tue esigenze specifiche:
- Usa librerie simple per la maggior parte delle tue componenti interne
- Usa librerie buildable quando hai bisogno di test più veloci o vuoi ridurre i tempi di build per applicazioni complesse
- Usa librerie publishable quando devi condividere codice anche al di fuori del tuo monorepo
La scelta della tipologia non è scolpita nella pietra, si può partire con un tipo “Simple” e passare a buildable o pushable in un secondo momento, in funzione della crescita del progetto.
Ma quindi come si crea una libreria?
nx generate @nx/angular:library library-name --directory=auth
Module Boundaries: mantenere l’ordine nel caos
Questo è un’altro aspetto che mi ha fatto innamorare di Nx: possiamo definire confini chiari e precisi tra i diversi progetti tramite il concetto di “module boundaries”. Questi confini aiutano a mantenere una struttura pulita e prevenire dipendenze circolari o indesiderate, ma soprattutto permettono all’architetto della soluzione di definire dei vincoli di progetto in modo semplice.
Imaginate questa situazione: il solution architect definisce che la libreria Core non può dipendere da nessun progetto. Poi arriva lo sviluppatore (si, arriva sempre) e decide di voler importare la libreria di dominio “Orders” all’interno della libreria Core. I module boundaries definiti dal solution architect si metteranno di mezzo impedendo allo sviluppatore di compiere questo atto impuro! Mi piace immaginare questa situazione così:

Tornando a noi, i module boundaries sono configurati nel file nx.JSON
o nei file .eslintrc.json
dei singoli progetti:
{
"extends": ["../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"rules": {
"@nx/enforce-module-boundaries": [
"error",
{
"enforceBuildableLibDependency": true,
"allow": [],
"depConstraints": [
{
"sourceTag": "type:app",
"onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:utility"]
},
{
"sourceTag": "type:feature",
"onlyDependOnLibsWithTags": ["type:ui", "type:utility", "type:data-access"]
},
{
"sourceTag": "type:ui",
"onlyDependOnLibsWithTags": ["type:ui", "type:utility"]
},
{
"sourceTag": "scope:admin",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:admin"]
},
{
"sourceTag": "scope:customer",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:customer"]
}
]
}
]
}
}
]
}
In principio definiamo le regole tramite gli scope ed i type:
- Regole basate sul tipo: Determinano quali tipi di librerie possono dipendere da altri tipi. Ad esempio, una libreria UI può dipendere solo da altre librerie UI o utility.
- Regole basate sullo scope: Determinano quali librerie possono dipendere da altre in base al loro ambito funzionale. Ad esempio, le librerie con scope “admin” possono dipendere solo da librerie con scope “shared” o “admin”.
Per applicare queste regole, devi assegnare i tag ai tuoi progetti nel file project.json
:
{
"name": "admin-dashboard",
"tags": ["type:feature", "scope:admin"]
}
Così facendo, se un developer prova a importare un componente da una libreria non consentita secondo queste regole, riceverà un errore durante l’analisi lint, prevenendo dipendenze indesiderate.
Conclusione
Con NX abbiamo un nuovo potente strumento da mettere nelle nostre giberne da sviluppatore, per affrontare nel migliore dei modi le sfide tecniche di ogni giorno.
Nx trasforma il modo in cui sviluppiamo applicazioni Angular (e non solo) di grandi dimensioni, offrendo strumenti potenti per gestire la complessità, migliorare la condivisione del codice e accelerare i cicli di sviluppo.
Se stai lavorando su un’applicazione Angular che cresce in complessità o gestisci più applicazioni correlate, Nx potrebbe essere proprio lo strumento che stavi cercando per portare il tuo sviluppo al livello successivo.
Per risolvere un problema complesso è necessario suddividerlo in tanti piccoli sotto-problemi. Per gestire un’applicazione complessa è necessario suddividere la sua architettura in sotto-progetti.
Alla prossima!