Vue 3 Composition API con TypeScript: pattern per applicazioni enterprise

Vue 3 Composition API con TypeScript: pattern per applicazioni enterprise

Negli ultimi due anni ho guidato la migrazione da Vue 2 Options API a Vue 3 Composition API con TypeScript di tre applicazioni enterprise di clienti italiani PMI - un gestionale logistica da 40.000 righe di frontend, un portale B2B marketplace da 28.000 righe, una dashboard operativa finanziaria da 18.000 righe. Le tre applicazioni erano state scritte fra 2019 e 2022 con Vue 2 senza TypeScript, con pattern tipici dell'epoca: Options API con data/methods/computed, Vuex 3.x per state management, propagazione eventi complessa fra componenti multilivello. Al momento della migrazione (settembre 2023 per la prima, maggio 2024 per la seconda, ottobre 2024 per la terza), la motivazione strategica era duplice: end-of-life di Vue 2 previsto per fine 2023, bisogno di type safety per ridurre bug di produzione ricorrenti.

Il pattern di migrazione che ho sviluppato nei primi mesi è stato progressivamente raffinato attraverso le tre esperienze. Al termine delle tre migrazioni, ho catalogato tre liste: i pattern che funzionano davvero a scala enterprise (replicabili con confidenza), i pattern che sembrano buoni online ma producono problemi su codebase grandi (da evitare), i pattern neutri che possono essere usati o evitati in funzione del contesto. Questo articolo è quel catalogo, utile per chi sta affrontando migrazione simile o sta iniziando un nuovo progetto Vue 3 con obiettivo enterprise.

Il principio guida è uno: la Composition API di Vue 3 è estremamente potente ma anche facilmente mal-usata in codebase grandi. I pattern tutorial-friendly (funzionano bene in esempi di 200 righe) spesso non scalano. La disciplina architetturale è più importante della sofisticazione del codice.

Perché la Composition API è superiore alla Options API per applicazioni enterprise

La Composition API è il nuovo paradigma di composizione dei componenti introdotto in Vue 3. Rispetto alla Options API classica (con sezioni data, computed, methods, watch separate), offre tre vantaggi sostanziali per applicazioni enterprise.

Primo vantaggio: riutilizzo della logica via composables. Nella Options API, condividere logica fra componenti richiedeva mixin - pattern con limitazioni strutturali (namespace conflict, implicit dependency, difficoltà debug). Nella Composition API, la logica riutilizzabile si estrae in funzioni composable (useUserPermissions(), useFormValidation(), useApiQuery()) che sono esplicite nei dati che ricevono e restituiscono - molto più debuggabili e testabili.

Secondo vantaggio: type inference migliore con TypeScript. La Options API richiedeva decoratori o casting espliciti per ottenere type safety decente. La Composition API si integra naturalmente con TypeScript - ref<User | null>(null) è tipizzato correttamente out of the box, e tutti i type guard flow attraverso il codice senza casting ridondante.

Terzo vantaggio: organizzazione logica invece che sintattica. Nella Options API, il codice era organizzato per tipo (tutti i data insieme, tutti i methods insieme) anche se la logica di una feature specifica era distribuita fra sezioni. Nella Composition API, si può raggruppare tutta la logica di una feature (state + computed + methods + watchers) in una sezione contigua, rendendo il codice più leggibile man mano che la complessità cresce.

La documentazione ufficiale Vue 3 Composition API è eccellente ed è il riferimento canonico per i concetti fondamentali.

Se stai pianificando una migrazione a Vue 3 di un'applicazione enterprise esistente o l'avvio di un nuovo progetto con requisiti di scala, nel mio profilo professionale trovi il dettaglio dei progetti frontend complessi che ho guidato in contesti PMI, con approccio di progettazione pragmatica basata su pattern che scalano.

Composables: il pattern che funziona e il pattern che non scala

Il concetto di composable è centrale nella Composition API. Una composable è una funzione che prende alcuni argomenti (opzionali), crea state reattivo, restituisce state e metodi che il componente consumerà. Sembra semplice ma ha varianti sottili che producono esiti diversi a scala.

Pattern che funziona a scala enterprise: composable pure con dependency injection esplicita.

export function useProductSearch(params: {
  httpClient: HttpClient;
  initialQuery?: string;
  debounceMs?: number;
}) {
  const query = ref(params.initialQuery ?? '');
  const results = ref<Product[]>([]);
  const isLoading = ref(false);
  const error = ref<Error | null>(null);

  const search = async (searchQuery: string) => {
    isLoading.value = true;
    error.value = null;
    try {
      const response = await params.httpClient.get<Product[]>(
        `/api/products?q=${encodeURIComponent(searchQuery)}`
      );
      results.value = response;
    } catch (e) {
      error.value = e instanceof Error ? e : new Error('Unknown error');
    } finally {
      isLoading.value = false;
    }
  };

  const debouncedSearch = debounce(search, params.debounceMs ?? 300);

  watch(query, (newQuery) => {
    if (newQuery.length >= 2) {
      debouncedSearch(newQuery);
    }
  });

  return { query, results, isLoading, error };
}

La caratteristica critica di questo pattern è la dependency injection esplicita - l'httpClient viene passato come parametro invece di essere importato direttamente. Questo rende il composable facilmente testabile (si passa un mock) e riutilizzabile in contesti diversi (staging, produzione, test con fixtures diverse).

Pattern che sembra buono ma non scala: composable con side effects global.

// ANTI-PATTERN
import { httpClient } from '@/services/http';
import { useToast } from '@/composables/useToast';

export function useProductSearch() {
  const toast = useToast();
  const query = ref('');
  // ...
  const search = async () => {
    try {
      // ...
    } catch (e) {
      toast.error('Search failed'); // side effect global diretto
    }
  };
  // ...
}

Il problema: il composable ha dipendenze implicite (httpClient, useToast) non dichiarate nella firma. Testarlo richiede mock globali complessi. Riutilizzarlo in contesti diversi (es. dashboard amministrativa vs app mobile) è difficile perché lui decide autonomamente come mostrare errori.

State management con Pinia: store per domini business, non per feature

Vue 3 raccomanda Pinia come successor di Vuex. Pinia è significativamente più leggero e type-friendly di Vuex, ma la strutturazione degli store richiede disciplina architetturale per non produrre codebase frammentato in codebase grandi.

Pattern che funziona: store organizzati per dominio business, non per feature UI. Uno store useUserStore gestisce identità utente, permessi, preferenze - usato da decine di componenti UI diversi. Uno store useOrdersStore gestisce il ciclo di vita degli ordini - usato da lista ordini, dettaglio ordine, form creazione ordine, dashboard operativa. Uno store useNotificationsStore gestisce le notifiche - utilizzato ovunque nel sistema.

Pattern che non scala: store per ogni componente ("useOrderListStore", "useOrderDetailStore", "useOrderFormStore"). Questo porta a duplicazione di logica, difficoltà di coordinamento fra store correlati, e esplosione del numero di store che nessuno riesce più a tracciare.

Il pattern di implementazione di uno store ben strutturato è:

import { defineStore } from 'pinia';

export const useOrdersStore = defineStore('orders', () => {
  const orders = ref<Order[]>([]);
  const currentOrder = ref<Order | null>(null);
  const isLoading = ref(false);

  const activeOrders = computed(() =>
    orders.value.filter(o => o.status !== 'archived')
  );

  async function fetchOrders(filters?: OrderFilters) {
    isLoading.value = true;
    try {
      orders.value = await orderApi.list(filters);
    } finally {
      isLoading.value = false;
    }
  }

  async function createOrder(data: CreateOrderDto) {
    const created = await orderApi.create(data);
    orders.value.unshift(created);
    return created;
  }

  return {
    orders: readonly(orders),
    currentOrder: readonly(currentOrder),
    isLoading: readonly(isLoading),
    activeOrders,
    fetchOrders,
    createOrder,
  };
});

Il pattern critico è readonly sui ref esposti - il componente può leggere orders.value ma non può modificarlo direttamente; le mutazioni devono passare dai metodi esposti (fetchOrders, createOrder). Questa disciplina previene un'intera classe di bug dove un componente modifica state condiviso senza che altri componenti si rendano conto.

Integrazione con API Laravel: tipi end-to-end e validazione runtime

Un pattern particolarmente valioso per applicazioni Vue 3 che consumano API Laravel è la tipizzazione end-to-end - i tipi TypeScript del frontend matchano esattamente i modelli del backend Laravel. Senza disciplina, frontend e backend divergono nel tempo e emergono bug sottili.

Il pattern che applico include tre elementi. Primo elemento: OpenAPI schema generato automaticamente dalle rotte Laravel (tramite Scribe o Scramble package), che definisce forma e tipi di ogni endpoint API. Secondo elemento: code generation dal schema OpenAPI a tipi TypeScript (tramite openapi-typescript o simili) - i tipi vengono rigenerati automaticamente ad ogni cambio del backend. Terzo elemento: validazione runtime con Zod dei payload ricevuti - anche con tipi corretti a compile time, validazione runtime cattura bug quando il backend cambia senza aggiornare lo schema.

Il pattern si integra con i principi di architettura moderna Laravel che ho descritto nel mio articolo sul dependency injection avanzato PHP 8 per servizi testabili e sostituibili, dove la disciplina di tipizzazione forte migliora la testabilità end-to-end.

Pattern di testing per Vue 3: Vitest + Vue Testing Library

Il testing di applicazioni Vue 3 si basa su Vitest come test runner e Vue Testing Library come helper di testing. La filosofia che applico è test behavioral invece di implementation-detail: i test verificano cosa l'utente vede e può fare, non i dettagli interni del componente. Questo approccio produce test robusti che sopravvivono a refactoring interni.

Per ogni componente significativo ho tre livelli di test. Primo livello: unit test del composable stesso, testato in isolamento senza rendering. Secondo livello: test del componente con mock degli store Pinia, verificando rendering condizionale e interazioni utente. Terzo livello: test end-to-end tramite Playwright su flussi critici attraverso l'applicazione completa. Questi si integrano con i pattern di testing che ho descritto nel mio articolo su testcontainers per PHP, dove il principio comune è testare vicino alla realtà di produzione.

Performance: tree shaking, code splitting, lazy loading delle route

Per applicazioni enterprise di dimensione significativa (30.000+ righe di frontend), la performance del bundle JavaScript diventa un problema reale. Un bundle da 5 MB è comune in applicazioni Vue 3 cresciute organicamente senza attenzione a bundling - rende la prima visualizzazione lenta e degrada l'esperienza utente.

Il pattern di ottimizzazione include tre tecniche. Prima tecnica: route-level code splitting. Ogni rotta è caricata dinamicamente (() => import('./views/OrdersPage.vue')), permettendo al bundler di dividere il codice in chunk separati. L'utente scarica solo il codice della pagina iniziale, gli altri arrivano on-demand. Seconda tecnica: lazy loading di librerie pesanti. Librerie come chart.js, date-fns, editor WYSIWYG - caricate solo nelle pagine che le usano effettivamente. Terza tecnica: tree shaking delle utility. Importare solo funzioni specifiche (import { debounce } from 'lodash-es') invece di librerie intere (import _ from 'lodash') riduce drammaticamente il bundle finale.

Gestione degli errori: boundaries, toast, reporting

La gestione degli errori in applicazioni Vue 3 enterprise richiede approccio strutturato. Il pattern operativo che applico combina quattro livelli. Primo livello: error handling nelle composable con propagazione esplicita tramite return object (errore come parte del return, non exception che esce). Secondo livello: error boundary a livello route - wrapper che cattura errori non gestiti e mostra UI di fallback invece di schermata bianca. Terzo livello: notifications utente via toast per errori recuperabili. Quarto livello: error reporting centralizzato tramite Sentry o equivalente - ogni errore non gestito viene logato con contesto sufficiente per debugging.

Il pattern si integra naturalmente con il modello di gestione errori del backend Laravel, creando una filosofia operativa unificata fra frontend e backend.

Migrazione da Vue 2: il pattern incrementale che riduce il rischio

La migrazione diretta da Vue 2 a Vue 3 di un'applicazione di 30.000+ righe è un progetto multimese significativo. Il pattern che ho perfezionato nelle tre migrazioni descritte all'inizio è migrazione incrementale con compatibility layer.

Fase 1: aggiornamento a Vue 2.7 (ultima versione Vue 2 con retroporting delle Composition API). Questo permette di iniziare a scrivere nuovo codice in Composition API senza ancora migrare esistente.

Fase 2: migrazione dipendenze third-party a versioni compatibili Vue 3. Ogni libreria UI usata (Element Plus, Vuetify, PrimeVue) va migrata alla sua versione Vue 3. Questa fase è spesso la più frustrante perché alcune librerie non offrono versioni compatibili e richiedono sostituzione.

Fase 3: migrazione componenti feature-by-feature da Options API a Composition API, mantenendo l'applicazione funzionante durante tutto il processo. Il codice vecchio e nuovo coesiste fino al completamento.

Fase 4: aggiornamento finale a Vue 3 con @vue/compat attivato per retrocompatibilità Vue 2 su rimanenze. Progressivamente rimuovere il compat layer.

Fase 5: cleanup finale - rimozione di @vue/compat, introduzione strict TypeScript ovunque, refactoring di pattern legacy.

Il pattern migratorio totale richiede tipicamente 6-12 mesi per applicazione di dimensione media, con investimento totale di 300-800 giornate-uomo distribuito su team di 3-5 sviluppatori.

Il risultato finale delle tre migrazioni ha prodotto dati misurabili comparabili. Riduzione dei bug di produzione nei 6 mesi post-migrazione: media del 45% rispetto ai 6 mesi pre-migrazione (grazie alla type safety). Tempo medio di sviluppo di nuove feature: ridotto del 20-30% (grazie a Composition API e composable riutilizzabili). Bundle size ottimizzato: riduzione del 30-50% sulla prima page load grazie a code splitting moderno. Onboarding di nuovi sviluppatori: tempo sceso da 4-6 settimane a 2-3 settimane grazie alla type safety che auto-documenta il codice.

Se guidi un team di sviluppo con applicazioni Vue 2 ancora in produzione o con applicazioni Vue 3 cresciute organicamente senza disciplina architetturale, l'investimento in pattern strutturati produce ROI misurabile in pochi mesi. Se vuoi confrontarti sul tuo caso specifico con un'analisi indipendente della tua codebase frontend, contattami per una consulenza preliminare: in una sessione di analisi guidata produciamo insieme una valutazione onesta del tuo stato architetturale attuale, identifichiamo i pattern problematici, e pianifichiamo un percorso di refactoring incrementale che non interrompe il ciclo di sviluppo produttivo.

Ultima modifica: