← Blog
typescriptmonoreporeact nativenode.jsarchitettura

Condividere i tipi tra quattro app

10 aprile 2026

Condividere i tipi tra quattro app

ReD Sposi è quattro applicazioni che si parlano:

  • App mobile — Expo + React Native, Expo Router, NativeWind
  • Backend API — Express + Node.js, MongoDB con Mongoose
  • Pannello admin — React + Vite, SPA
  • Landing page — Next.js

Tutte e quattro usano TypeScript. Tutte e quattro parlano con le stesse API. Tutti e quattro modellano le stesse entità: invitati, RSVP, foto, messaggi di chat, scommesse.

La domanda inevitabile è: come tieni allineati i tipi?

Le opzioni sul tavolo

Esistono tre approcci principali.

Package condiviso. Crei un package @red-sposi/types nel monorepo, ci metti tutte le interfacce condivise, e ogni app lo importa come dipendenza. È l'approccio più pulito architetturalmente — un'unica fonte di verità, nessuna duplicazione.

Validazione runtime con Zod. Definisci gli schemi con Zod, e da ogni schema inferisci il tipo TypeScript. Il vantaggio è che la validazione avviene a runtime — se il backend manda un campo in più o in meno, lo sai immediatamente, non solo al compile time.

Copy-paste consapevole. Definisci i tipi dove ha più senso (di solito sul backend, vicino ai modelli Mongoose), e li copi manualmente nelle app che li usano. È tecnicamente il meno elegante, ma ha zero overhead di configurazione.

Cosa ho scelto e perché

Per ReD Sposi ho scelto una via di mezzo tra il package condiviso e il copy-paste: tipi definiti sul backend, esportati da un file dedicato, e copiati manualmente nel frontend quando cambiano.

Non è la scelta più elegante. È la scelta con il costo di setup più basso per un progetto con una scadenza fissa, costruito da una persona sola.

Un package condiviso richiede configurazione del monorepo — path aliases, build pipeline, risoluzione dei moduli in Expo e Next.js. Expo in particolare ha alcune limitazioni su come risolve i moduli esterni al suo root, che richiedono configurazione aggiuntiva nel metro.config.js e nel tsconfig.json. Non è impossibile, ma è un'ora di setup che non aggiunge funzionalità.

Zod è ottimo — lo uso sul backend per validare i body delle richieste — ma usarlo come source of truth per i tipi condivisi aggiunge un layer di complessità che per questo progetto non si ripagava.

Come funziona in pratica

Sul backend, i tipi vivono vicino ai modelli Mongoose:

// models/Guest.ts
export interface IGuest {
  _id: string;
  name: string;
  code: string;
  rsvp?: {
    confirmed: boolean;
    menuChoice: "standard" | "vegetarian" | "vegan";
    shuttle: boolean;
    completedAt: Date;
  };
  pushToken?: string;
}

const GuestSchema = new Schema<IGuest>({ ... });
export const Guest = model<IGuest>("Guest", GuestSchema);

Sul frontend, importo direttamente questo tipo o ne definisco uno derivato per le esigenze specifiche dell'UI:

// app mobile: tipi locali che rispecchiano il backend
export interface Guest {
  _id: string;
  name: string;
  rsvp?: {
    confirmed: boolean;
    menuChoice: "standard" | "vegetarian" | "vegan";
    shuttle: boolean;
  };
}

Non è DRY nel senso stretto. Ma per un'app con una decina di entità principali e una vita di qualche mese, è sufficiente.

Il vero problema: la deriva dei tipi

La criticità non è nel setup — è nel mantenimento. I tipi si desincronizzano quando cambiano i modelli del backend e il frontend non viene aggiornato.

In pratica questo si manifesta in due modi: errori TypeScript che non spiegano chiaramente cosa è cambiato, o — peggio — nessun errore perché il tipo è any o la risposta dell'API non viene validata.

Ho mitigato questo in due modi:

  1. Validazione al confine. Sul backend, ogni risposta dell'API è tipizzata esplicitamente. Se aggiungo un campo al modello, devo aggiungerlo anche al tipo della risposta — e Zod valida che i dati in ingresso corrispondano.

  2. Test di integrazione leggeri. Non ho test unitari su tutto, ma ho alcuni test che chiamano le API reali e verificano la forma della risposta. Se cambia un campo, il test fallisce, e so che c'è qualcosa da aggiornare sul frontend.

Quando avrei scelto diversamente

Se ReD Sposi fosse un prodotto long-term con una roadmap, avrei investito nel package condiviso dall'inizio. Il costo di setup si ammortizza su un orizzonte lungo — ogni modifica al modello dei dati che non richiede aggiornamenti manuali in tre posti diversi è tempo risparmiato.

Per un progetto che finisce ad agosto, il calcolo era diverso. Il costo del setup avanzato era certo; il beneficio dipendeva da quante volte avrei cambiato i modelli in modo significativo. Con una scadenza ravvicinata e una feature list abbastanza stabile, la complessità aggiuntiva non si ripagava.

La lezione

Non esiste un'architettura giusta in assoluto — esiste un'architettura giusta per il contesto in cui si lavora.

Un package condiviso è la risposta corretta per un prodotto long-term con un team. Il copy-paste consapevole è la risposta corretta per un progetto con scadenza fissa costruito da solo. Zod come source of truth è la risposta corretta se la validazione runtime è un requisito di prodotto.

La scelta sbagliata è quella che risolve il problema teorico invece del problema reale.