Introduction : Pourquoi une Stack Technique Rigoureuse pour le Voyage
Dans le secteur du tourisme en ligne, la fiabilité des données est aussi cruciale que la rapidité d'affichage. Un comparateur de voyages manipule des flux complexes : horaires de trains, prix dynamiques, disponibilités en temps réel et multiples devises. Une erreur de type dans le code peut se traduire par un billet vendu à un prix erroné ou un horaire incorrect, ce qui est inacceptable pour l'utilisateur final.
C'est pourquoi, pour la refonte de notre plateforme en 2026, nous avons opté pour une architecture basée sur Next.js 14 avec l'App Router, couplée à une configuration TypeScript en mode strict. Cette combinaison nous permet de bénéficier des avantages du rendu serveur (Server Components) pour le SEO, tout en imposant une sécurité de typage qui réduit drastiquement les bugs en production. Selon la documentation officielle de Next.js, l'App Router représente l'évolution fondamentale pour la gestion des routes et des layouts, permettant une granularité fine sur le caching et le streaming des données.

Dans cet article, nous allons détailler les choix techniques qui sous-tendent notre comparateur, en nous concentrant sur la configuration TypeScript, la gestion des données via les Server Actions, et l'optimisation des performances web vitales.
Next.js 14 et l'App Router : Le Cœur du Système
L'adoption de l'App Router dans Next.js 14 marque un changement de paradigme par rapport au Pages Router historique. Pour un comparateur de voyages, où la structure des pages est hiérarchique (Accueil > Destination > Transport > Détail), le système de routage basé sur le système de fichiers de l'App Router offre une clarté indispensable.
Server Components par Défaut
L'un des avantages majeurs est l'utilisation des React Server Components (RSC) par défaut. Dans notre comparateur, les listes de résultats de recherche (trains, bus, vols) sont rendues côté serveur. Cela signifie que le code de récupération des données s'exécute directement sur le serveur, près de la base de données ou des API tierces (SNCF, DB, Eurail). Cela élimine le besoin d'un état de chargement visible par l'utilisateur pour le contenu initial et améliore le Largest Contentful Paint (LCP), une métrique clé des Core Web Vitals définie par Google.
Contrairement aux approches précédentes où le client devait fetcher les données via useEffect, nous utilisons maintenant des composants asynchrones directement dans la hiérarchie des routes. Voici un exemple simplifié de notre page de résultats :
// 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>Résultats pour {searchParams.from} vers {searchParams.to}</h1>
<TripList trips={trips} />
</main>
);
}
Cette approche réduit le bundle JavaScript envoyé au client, car la logique de fetch n'est pas incluse dans le code téléchargé par le navigateur. Pour un utilisateur sur un réseau mobile en voyage, cette économie de données et de temps de traitement est significative.
Gestion des Layouts Nested
La structure de notre application nécessite des layouts persistants, comme la barre de navigation et le footer, qui ne doivent pas se recharger lors de la navigation entre les pages de résultats. L'App Router gère cela nativement via le fichier layout.tsx. De plus, nous pouvons avoir des layouts spécifiques pour certaines sections, comme la section "Nightjet" ou "Eurail", permettant d'injecter des scripts ou des styles spécifiques sans affecter le reste de l'application.
TypeScript Strict : Une Sécurité Indispensable
Utiliser TypeScript est une chose, l'utiliser en mode strict en est une autre. Dans notre tsconfig.json, nous activons toutes les options de strictesse. Cela inclut noImplicitAny, strictNullChecks, et strictFunctionTypes. Pour un comparateur qui agrège des données provenant de multiples API hétérogènes, la sécurité de type est notre première ligne de défense.
Configuration tsconfig.json
Voici la base de notre configuration TypeScript, alignée avec les recommandations de 2026 pour les projets 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'option strict: true force le développeur à gérer explicitement les cas où une variable pourrait être undefined ou null. Dans le contexte du voyage, un prix peut être nul (offre gratuite), une heure de départ peut manquer (trajet incomplet). TypeScript nous oblige à gérer ces cas explicitement, évitant les erreurs classiques de type Cannot read property of undefined.
Typage des Variables d'Environnement
Une erreur courante dans les projets Next.js est l'utilisation non typée des variables d'environnement. Nous utilisons une bibliothèque comme t3-env ou un schéma Zod personnalisé pour valider les variables au démarrage du serveur. Cela garantit que si une clé API pour un fournisseur de train (ex: API DB ou SNCF) est manquante, l'application refuse de démarrer plutôt que de échouer silencieusement en production.
// 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,
},
});
Cette pratique, recommandée par la communauté Vercel et les mainteneurs de Next.js, assure que notre configuration est aussi robuste que notre code.
Validation des Données avec Zod et Server Actions
Dans une architecture App Router, les Server Actions remplacent souvent les API Routes traditionnelles pour les mutations de données (comme la réservation ou la sauvegarde d'une alerte prix). Cependant, les arguments passés à une Server Action ne sont pas typés par défaut lors de l'appel client. C'est ici que la validation de schéma devient critique.
Nous utilisons Zod pour valider les entrées utilisateur avant toute traitement métier. Cela protège contre les injections et les données malformées. Par exemple, lors de la recherche d'un trajet, nous devons nous assurer que les dates sont valides et que les codes de gare existent.

Exemple de Server Action Typée
// actions/search.ts
'use server';
import { z } from 'zod';
import { db } from '@/lib/db';
const SearchSchema = z.object({
origin: z.string().length(3), // Code IATA ou 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: 'Données invalides', details: result.error.flatten() };
}
// Traitement métier sécurisé
const trips = await db.trip.findMany({
where: {
origin: result.data.origin,
// ... autres filtres
}
});
return { success: true, data: trips };
}
En combinant TypeScript strict et Zod, nous obtenons une validation statique (à la compilation) et dynamique (à l'exécution). C'est ce que nous appelons le "type safety end-to-end". Selon les bonnes pratiques TypeScript 2026, cette double validation est essentielle pour les applications critiques.
Performance et SEO : Les Enjeux du Comparateur
Un comparateur de voyages vit par son référencement organique (SEO). Les utilisateurs cherchent "Train Paris Berlin" ou "Nightjet Bruxelles". Next.js 14 excelle dans ce domaine grâce au rendu serveur statique (Static Site Generation) et au rendu dynamique hybride.
Optimisation des Images et Fonts
Nous utilisons le composant next/image pour optimiser automatiquement les visuels des trains et des destinations. Cela permet de servir des formats modernes comme WebP ou AVIF selon le navigateur de l'utilisateur. De plus, l'utilisation de next/font pour charger les polices (comme Inter ou Roboto) élimine le layout shift (CLS), garantissant que le texte ne bouge pas pendant le chargement.
Streaming et Suspense
Pour les pages de résultats complexes, nous utilisons React Suspense. Cela permet d'afficher immédiatement le squelette de la page (header, filtres) tandis que les résultats de recherche, qui peuvent prendre quelques secondes à agréger depuis plusieurs fournisseurs (Ouigo, TGV Inoui, DB), chargent en arrière-plan. L'utilisateur peut commencer à interagir avec les filtres avant même que la liste complète des trains ne soit affichée.
Cette technique améliore considérablement le Time to Interactive (TTI). Dans un contexte concurrentiel où chaque seconde compte pour la conversion, cette optimisation technique se traduit directement par une augmentation du taux de réservation.
Internationalization (i18n) et Maintenance
Notre comparateur vise un public européen. La gestion de plusieurs langues (français, allemand, anglais, espagnol) est native dans notre structure de routes. Next.js permet de préfixer les routes par la locale (/fr/search, /de/search).
Avec TypeScript, nous typons les clés de traduction. Cela signifie que si un développeur ajoute un nouveau texte dans l'interface mais oublie de mettre à jour le fichier de traduction allemand, le compilateur levera une erreur. Cela évite les situations embarrassantes où une partie de l'interface reste en anglais sur une page destinée au marché allemand.
La maintenance du code est également facilitée par la modularité de l'App Router. Chaque fonctionnalité (recherche, paiement, compte utilisateur) est isolée dans son propre dossier avec ses propres tests. Nous utilisons Vitest pour les tests unitaires et Playwright pour les tests end-to-end, assurant que les mises à jour de dépendances (comme la migration vers React 19 prévue dans l'écosystème Next.js) ne cassent pas les fonctionnalités existantes.
Conclusion : Une Base Solide pour l'Avenir
Le choix de Next.js 14 et de TypeScript strict n'est pas seulement une tendance technique, c'est une nécessité opérationnelle pour un comparateur de voyages en 2026. La complexité des données ferroviaires et aériennes exige une rigueur que le typage statique impose naturellement. L'App Router offre la flexibilité nécessaire pour composer des interfaces riches tout en maintenant des performances de premier ordre.
En suivant les recommandations de la documentation Next.js et en appliquant des standards stricts de qualité de code, nous avons construit une plateforme capable d'évoluer. Que ce soit pour intégrer de nouveaux fournisseurs comme les lignes Nightjet étendues ou pour supporter de nouvelles réglementations européennes sur les données voyageurs, notre architecture est prête. La technique sert ici l'expérience utilisateur : un site plus rapide, plus fiable et plus sécurisé pour préparer vos prochains voyages à travers l'Europe.
Pour aller plus loin, nous vous recommandons de consulter la documentation officielle de Next.js sur l'App Router et le guide de configuration TypeScript pour des mises à jour régulières sur les meilleures pratiques émergentes.


