Gopaxo

· di Rédaction

Architettura Tecnica: Costruire un Comparatore di Viaggi con Next.js 14 e TypeScript Strict

Scopri come abbiamo sviluppato la nostra piattaforma di comparazione viaggi utilizzando Next.js 14 App Router e TypeScript in modalità strict per garantire prestazioni e affidabilità.

Introduzione: Perché uno Stack Tecnologico Rigoroso per i Viaggi

Nel settore del turismo online, l'affidabilità dei dati è cruciale quanto la velocità di visualizzazione. Un comparatore di viaggi gestisce flussi complessi: orari dei treni, prezzi dinamici, disponibilità in tempo reale e multiple valute. Un errore di tipo nel codice può tradursi in un biglietto venduto a un prezzo errato o un orario incorretto, il che è inaccettabile per l'utente finale.

È per questo che, per la rifacimento della nostra piattaforma nel 2026, abbiamo optato per un'architettura basata su Next.js 14 con l'App Router, accoppiata a una configurazione TypeScript in modalità strict. Questa combinazione ci permette di beneficiare dei vantaggi del rendering lato server (Server Components) per la SEO, imponendo al contempo una sicurezza dei tipi che riduce drasticamente i bug in produzione. Secondo la documentazione ufficiale di Next.js, l'App Router rappresenta l'evoluzione fondamentale per la gestione delle route e dei layout, permettendo una granularità fine sul caching e sullo streaming dei dati.

Configurazione tecnica di un progetto Next.js moderno

In questo articolo, dettaglieremo le scelte tecniche che sostengono il nostro comparatore, concentrandoci sulla configurazione TypeScript, la gestione dei dati tramite le Server Actions e l'ottimizzazione delle prestazioni web vitali.

Next.js 14 e l'App Router: Il Cuore del Sistema

L'adozione dell'App Router in Next.js 14 segna un cambiamento di paradigma rispetto allo storico Pages Router. Per un comparatore di viaggi, dove la struttura delle pagine è gerarchica (Home > Destinazione > Trasporto > Dettaglio), il sistema di routing basato sul file system dell'App Router offre una chiarezza indispensabile.

Server Components di Default

Uno dei vantaggi principali è l'utilizzo dei React Server Components (RSC) di default. Nel nostro comparatore, le liste dei risultati di ricerca (treni, bus, voli) sono renderizzate lato server. Questo significa che il codice di recupero dati viene eseguito direttamente sul server, vicino al database o alle API di terze parti (SNCF, DB, Eurail). Ciò elimina la necessità di uno stato di caricamento visibile all'utente per il contenuto iniziale e migliora il Largest Contentful Paint (LCP), una metrica chiave dei Core Web Vitals definita da Google.

A differenza degli approcci precedenti dove il client doveva fetchare i dati via useEffect, ora utilizziamo componenti asincroni direttamente nella gerarchia delle route. Ecco un esempio semplificato della nostra pagina dei risultati:

// app/search/results/page.tsx
import { searchTrips } from '@/lib/api';
import { TripList } from './trip-list';

export default async function SearchResults({ searchParams }: { searchParams: { from: string; to: string } }) {
  const trips = await searchTrips(searchParams.from, searchParams.to);
  
  return (
    <main>
      <h1>Risultati per {searchParams.from} verso {searchParams.to}</h1>
      <TripList trips={trips} />
    </main>
  );
}

Questo approccio riduce il bundle JavaScript inviato al client, poiché la logica di fetch non è inclusa nel codice scaricato dal browser. Per un utente su una rete mobile in viaggio, questo risparmio di dati e tempo di elaborazione è significativo.

Gestione dei Layout Nested

La struttura della nostra applicazione richiede layout persistenti, come la barra di navigazione e il footer, che non devono ricaricarsi durante la navigazione tra le pagine dei risultati. L'App Router gestisce questo nativamente tramite il file layout.tsx. Inoltre, possiamo avere layout specifici per alcune sezioni, come la sezione "Nightjet" o "Eurail", permettendo di iniettare script o stili specifici senza influenzare il resto dell'applicazione.

TypeScript Strict: Una Sicurezza Indispensabile

Utilizzare TypeScript è una cosa, usarlo in modalità strict ne è un'altra. Nel nostro tsconfig.json, attiviamo tutte le opzioni di strictness. Questo include noImplicitAny, strictNullChecks e strictFunctionTypes. Per un comparatore che aggrega dati provenienti da multiple API eterogenee, la sicurezza dei tipi è la nostra prima linea di difesa.

Configurazione tsconfig.json

Ecco la base della nostra configurazione TypeScript, allineata con le raccomandazioni del 2026 per i progetti enterprise:

{
  "compilerOptions": {
    "target": "ES2022",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [{ "name": "next" }]
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

L'opzione strict: true forza lo sviluppatore a gestire esplicitamente i casi in cui una variabile potrebbe essere undefined o null. Nel contesto dei viaggi, un prezzo può essere nullo (offerta gratuita), un orario di partenza può mancare (tratta incompleta). TypeScript ci obbliga a gestire questi casi esplicitamente, evitando gli errori classici di tipo Cannot read property of undefined.

Tipizzazione delle Variabili d'Ambiente

Un errore comune nei progetti Next.js è l'utilizzo non tipizzato delle variabili d'ambiente. Utilizziamo una libreria come t3-env o uno schema Zod personalizzato per validare le variabili all'avvio del server. Questo garantisce che se una chiave API per un fornitore di treni (es: API DB o SNCF) manca, l'applicazione rifiuta di avviarsi invece di fallire silenziosamente in produzione.

// env.ts
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

export const env = createEnv({
  server: {
    DATABASE_URL: z.string().url(),
    RAIL_API_KEY: z.string().min(1),
  },
  client: {
    NEXT_PUBLIC_ANALYTICS_ID: z.string().min(1),
  },
  runtimeEnv: {
    DATABASE_URL: process.env.DATABASE_URL,
    RAIL_API_KEY: process.env.RAIL_API_KEY,
    NEXT_PUBLIC_ANALYTICS_ID: process.env.NEXT_PUBLIC_ANALYTICS_ID,
  },
});

Questa pratica, raccomandata dalla comunità Vercel e dai maintainer di Next.js, assicura che la nostra configurazione sia robusta quanto il nostro codice.

Validazione dei Dati con Zod e Server Actions

In un'architettura App Router, le Server Actions sostituiscono spesso le API Routes tradizionali per le mutazioni di dati (come la prenotazione o il salvataggio di un alert prezzo). Tuttavia, gli argomenti passati a una Server Action non sono tipizzati di default durante la chiamata client. È qui che la validazione dello schema diventa critica.

Utilizziamo Zod per validare gli input utente prima di qualsiasi elaborazione business. Questo protegge contro le injection e i dati malformati. Ad esempio, durante la ricerca di una tratta, dobbiamo assicurarci che le date siano valide e che i codici stazione esistano.

Validazione dei dati e flusso server

Esempio di Server Action Tipizzata

// actions/search.ts
'use server';

import { z } from 'zod';
import { db } from '@/lib/db';

const SearchSchema = z.object({
  origin: z.string().length(3), // Codice IATA o UIC
  destination: z.string().length(3),
  date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
  passengers: z.number().min(1).max(9),
});

export async function searchTrip(formData: FormData) {
  const rawData = {
    origin: formData.get('origin'),
    destination: formData.get('destination'),
    date: formData.get('date'),
    passengers: Number(formData.get('passengers')),
  };

  const result = SearchSchema.safeParse(rawData);

  if (!result.success) {
    return { error: 'Dati non validi', details: result.error.flatten() };
  }

  // Elaborazione business sicura
  const trips = await db.trip.findMany({
    where: {
      origin: result.data.origin,
      // ... altri filtri
    }
  });

  return { success: true, data: trips };
}

Combinando TypeScript strict e Zod, otteniamo una validazione statica (alla compilazione) e dinamica (all'esecuzione). Questo è ciò che chiamiamo "type safety end-to-end". Secondo le best practice TypeScript 2026, questa doppia validazione è essenziale per le applicazioni critiche.

Prestazioni e SEO: Le Sfide del Comparatore

Un comparatore di viaggi vive grazie alla sua indicizzazione organica (SEO). Gli utenti cercano "Treno Parigi Berlino" o "Nightjet Bruxelles". Next.js 14 eccelle in questo ambito grazie al rendering lato server statico (Static Site Generation) e al rendering dinamico ibrido.

Ottimizzazione di Immagini e Font

Utilizziamo il componente next/image per ottimizzare automaticamente i visual dei treni e delle destinazioni. Questo permette di servire formati moderni come WebP o AVIF a seconda del browser dell'utente. Inoltre, l'uso di next/font per caricare i font (come Inter o Roboto) elimina il layout shift (CLS), garantendo che il testo non si sposti durante il caricamento.

Streaming e Suspense

Per le pagine dei risultati complesse, utilizziamo React Suspense. Questo permette di visualizzare immediatamente lo scheletro della pagina (header, filtri) mentre i risultati di ricerca, che possono richiedere alcuni secondi per essere aggregati da diversi fornitori (Ouigo, TGV Inoui, DB), caricano in background. L'utente può iniziare a interagire con i filtri prima ancora che la lista completa dei treni sia visualizzata.

Questa tecnica migliora considerevolmente il Time to Interactive (TTI). In un contesto competitivo dove ogni secondo conta per la conversione, questa ottimizzazione tecnica si traduce direttamente in un aumento del tasso di prenotazione.

Internationalization (i18n) e Manutenzione

Il nostro comparatore mira a un pubblico europeo. La gestione di multiple lingue (francese, tedesco, inglese, spagnolo) è nativa nella nostra struttura di route. Next.js permette di prefissare le route per locale (/fr/search, /de/search).

Con TypeScript, tipizziamo le chiavi di traduzione. Questo significa che se uno sviluppatore aggiunge un nuovo testo nell'interfaccia ma dimentica di aggiornare il file di traduzione tedesco, il compilatore solleverà un errore. Questo evita situazioni imbarazzanti dove una parte dell'interfaccia rimane in inglese su una pagina destinata al mercato tedesco.

La manutenzione del codice è inoltre facilitata dalla modularità dell'App Router. Ogni funzionalità (ricerca, pagamento, account utente) è isolata nella propria cartella con i propri test. Utilizziamo Vitest per i test unitari e Playwright per i test end-to-end, assicurando che gli aggiornamenti delle dipendenze (come la migrazione a React 19 prevista nell'ecosistema Next.js) non rompano le funzionalità esistenti.

Conclusione: Una Base Solida per il Futuro

La scelta di Next.js 14 e TypeScript strict non è solo una tendenza tecnica, è una necessità operativa per un comparatore di viaggi nel 2026. La complessità dei dati ferroviari e aerei exige un rigore che la tipizzazione statica impone naturalmente. L'App Router offre la flessibilità necessaria per comporre interfacce ricche mantenendo prestazioni di primo ordine.

Seguendo le raccomandazioni della documentazione Next.js e applicando standard strict di qualità del codice, abbiamo costruito una piattaforma capace di evolversi. Che si tratti di integrare nuovi fornitori come le linee Nightjet estese o di supportare nuove regolamentazioni europee sui dati viaggiatori, la nostra architettura è pronta. La tecnica serve qui l'esperienza utente: un sito più veloce, più affidabile e più sicuro per preparare i vostri prossimi viaggi attraverso l'Europa.

Per approfondire, vi raccomandiamo di consultare la documentazione ufficiale di Next.js sull'App Router e la guida di configurazione TypeScript per aggiornamenti regolari sulle best practice emergenti.

Rotte ferroviarie popolari

Prenota biglietti del treno economici e premium sulle tratte più cercate.