Architettura frontend · Migrazione incrementale · 2026 · misurata da giugno a settembre

Guidare una migrazione graduale da Razor a React senza fermare un prodotto attivo

Il prodotto era una piattaforma ASP.NET Core multi-tenant in produzione, con un'interfaccia distribuita tra MVC, Razor, JavaScript/jQuery e alcuni componenti Blazor. Una riscrittura non era realistica, quindi ho guidato la direzione frontend di una migrazione incrementale a React + TypeScript: navigazione ibrida, route possedute dalle feature, un BFF same-origin, una struttura pagina riutilizzabile e guard di build, così che interfaccia legacy e React potessero convivere finché ogni area di business non fosse pronta per essere sostituita.

Il mio ruolo
Architettura frontend e direzione dell'implementazione: mie. Le feature di business sulla piattaforma: tutto il team.
Stack
  • React
  • TypeScript
  • React Router
  • Redux Toolkit
  • RTK Query
  • Vite (library mode)
  • ASP.NET Core
  • BFF

In sintesi

  • Le route React reali sono passate da 10 a 112 tra la baseline di giugno e lo snapshot di settembre
  • Artefatto JavaScript di entry da 1.764.861 a 63.249 byte (solo l'entry chunk)
  • 108 route reali sulla linea di staging, 65 delle quali mappate esplicitamente su URL legacy

Contesto

Il prodotto era una piattaforma ASP.NET Core multi-tenant già utilizzata, con un'interfaccia cresciuta tra MVC, Razor, JavaScript/jQuery e alcuni componenti Blazor. Una riscrittura completa non era realistica: i clienti dipendevano dall'applicazione esistente, lo sviluppo delle funzionalità doveva continuare e molte aree legacy erano accoppiate ad autenticazione, contesto tenant e flussi operativi.

I vincoli erano concreti. Il prodotto doveva restare utilizzabile per tutta la durata. Navigazione legacy e React dovevano convivere senza rompere autorizzazioni e deep link. Il browser non doveva mai ricevere il bearer token dell'API downstream. Le nuove feature avevano bisogno di pattern ripetibili per stato, contratti API, layout ed error handling. E ogni cambiamento passava da testing e staging prima della produzione, quindi doveva essere compatibile e facile da annullare.

Cosa ho fatto

  • Ho montato React e TypeScript sotto /app nello stesso host ASP.NET, con React Router che usa /app come basename mentre ASP.NET continua a gestire sessione e integrazioni server-side.
  • Ho impostato il flusso browser → BFF same-origin: il JWT dell'API resta nella sessione server-side, React riceve uno snapshot di autenticazione e usa la protezione CSRF per le mutation. Redux Toolkit e RTK Query sono diventati il modello condiviso per server state, caching ed endpoint definiti per feature.
  • Ho progettato la navigazione ibrida — pagina legacy, risoluzione automatica o route React esplicita — con utility condivise che preservano query string e hash e gestiscono destinazioni SPA, full-page ed esterne attraverso una sola interfaccia.
  • Ho spostato la route discovery su definizioni possedute dalle feature tramite import.meta.glob, con controlli sui duplicati, così una feature normale non modifica più un registry centrale.
  • Ho creato PageShell, un layout riutilizzabile con header, subheader opzionale e body scrollabile: nelle pagine operative desktop azioni e filtri restano visibili mentre scorre l'area dati; sugli schermi piccoli torna lo scroll naturale.
  • Ho implementato la separazione di route e vendor nella build Vite in library mode conservando il bridge ASP.NET, ho mantenuto un unico foglio di stile (un CSS separato poteva restare orfano in questo assetto) e ho aggiunto un verificatore del bundle che fa fallire la build se manca un chunk referenziato.

Decisioni

Incrementale, nello stesso host

Nessuna seconda applicazione e nessun cutover big-bang. React vive sotto /app nell'host ASP.NET esistente, così ogni area di business migra quando è sicuro farlo e la pagina legacy resta disponibile fino a quel momento.

Il token resta sul server

Il BFF tiene il JWT dell'API nella sessione server-side; il browser riceve soltanto uno snapshot di autenticazione e un token CSRF per le mutation.

Route possedute dalle feature

Le definizioni delle route vivono accanto alla feature e vengono scoperte in fase di build. Il registry centrale era diventato un punto caldo per i merge e un posto facile in cui dimenticare una voce.

JavaScript separato, un solo foglio di stile

Code splitting per route e vendor, ma un bundle CSS unico: in library mode un foglio di stile separato poteva non venire mai caricato. Il verificatore intercetta un chunk mancante al rilascio invece che in produzione.

La difficoltà concreta

Con la crescita della superficie React, BFF e routing sono diventati la principale fonte di rischio d'integrazione: da un lato una grande superficie centrale di endpoint, registrazioni DI che potevano divergere tra produzione e test, header HTTP condivisi e disallineamenti tra contratti client e server; dall'altro resolver duplicati e destinazioni incoerenti.

Stabilizzare ha significato endpoint, servizi e DTO per feature con una composizione DI condivisa, header HTTP per richiesta, controlli di inventario client/server, verifica dei contratti Swagger, navigazione centralizzata e controlli del bundle al rilascio. Ho lavorato con il responsabile della promozione delle branch e sono rimasto coinvolto durante lo staging finché gli errori frontend non hanno raggiunto un livello operativo stabile.

Risultato

  • Tra la baseline verificata di giugno e lo snapshot di settembre, le route React reali sono passate da 10 a 112 sull'HEAD; la linea di staging esaminata conteneva 108 route reali, escludendo 42 placeholder e due route solo di sviluppo.
  • Delle 108 route di staging, 65 erano mappate esplicitamente su URL legacy, 16 dichiarate come route React native e 27 erano percorsi aggiuntivi di dettaglio, creazione, modifica o comunque senza mapping.
  • Le pagine React promosse in staging risultavano operative secondo l'osservazione diretta durante il lavoro; il repository conteneva 317 file di test frontend e il tracker della migrazione ha superato 16 test su 16.
  • Il risultato durevole è una piattaforma di migrazione: route possedute dalle feature, navigazione condivisa, struttura pagina riutilizzabile, confini BFF, pattern coerenti per lo stato e guard di rilascio che hanno permesso al lavoro React di crescere mentre il prodotto legacy restava disponibile.

Numeri, con i loro limiti

  • Route React reali sull'HEADPrima10Dopo112baseline di giugno → snapshot di settembre 2026
  • Route reali sulla linea di staging108escludendo 42 placeholder e 2 route solo di sviluppo
  • Route di staging mappate su un URL legacy65 su 10816 route React native, 27 percorsi di dettaglio/creazione/modifica o senza mapping
  • Artefatto JavaScript di entryPrima1.764.861 byteDopo63.249 bytesolo l'entry chunk, non il JavaScript totale
  • File di test frontend · tracker della migrazione317 file · 16 / 16 testconteggi storici, settembre 2026

Cosa questi numeri non dicono

  • Una route non è una feature di business completa, e il numero di route non dice quanta parte del prodotto legacy sia stata migrata.
  • La riduzione dell'entry chunk non è una riduzione del JavaScript totale trasferito né dei tempi di caricamento: nessuno dei due è stato misurato.
  • L'operatività in staging è un'osservazione ingegneristica, non uno smoke test route per route, un'attivazione per i clienti o un'adozione in produzione. Questi dati non sono stati conservati come metriche verificabili, per questo non presento il lavoro come una migrazione completata.

Cosa dimostra

So progettare un percorso di migrazione che lasci crescere un nuovo stack dentro un prodotto legacy in produzione — con confini di sicurezza, pattern ripetibili e guard di rilascio — restando responsabile fino alla stabilizzazione.

Attribuzione

Ho guidato architettura frontend e direzione dell'implementazione, usando l'assistenza dell'AI per analisi del repository e implementazione mantenendo decisioni, review e release readiness. Le feature di business costruite sulla piattaforma appartengono a tutto il team, e le scelte sulla libreria UI sono state decisioni condivise.