Gopaxo

· por Rédaction

Arquitectura Técnica: Construir un Comparador de Viajes con Next.js 14 y TypeScript Strict

Descubre cómo desarrollamos nuestra plataforma de comparación de viajes utilizando Next.js 14 App Router y TypeScript en modo estricto para garantizar rendimiento y fiabilidad.

Introducción: Por qué un Stack Técnico Riguroso para los Viajes

En el sector del turismo en línea, la fiabilidad de los datos es tan crucial como la rapidez de visualización. Un comparador de viajes maneja flujos complejos: horarios de trenes, precios dinámicos, disponibilidades en tiempo real y múltiples divisas. Un error de tipo en el código puede traducirse en un billete vendido a un precio erróneo o un horario incorrecto, lo cual es inaceptable para el usuario final.

Por eso, para el rediseño de nuestra plataforma en 2026, optamos por una arquitectura basada en Next.js 14 con el App Router, coupled con una configuración TypeScript en modo estricto. Esta combinación nos permite beneficiarnos de las ventajas del renderizado del lado del servidor (Server Components) para el SEO, mientras impone una seguridad de tipos que reduce drásticamente los bugs en producción. Según la documentación oficial de Next.js, el App Router representa la evolución fundamental para la gestión de rutas y layouts, permitiendo una granularidad fina sobre el caching y el streaming de datos.

Configuración técnica de un proyecto Next.js moderno

En este artículo, detallaremos las elecciones técnicas que sustentan nuestro comparador, centrándonos en la configuración de TypeScript, la gestión de datos mediante Server Actions, y la optimización de las métricas web vitales.

Next.js 14 y el App Router: El Corazón del Sistema

La adopción del App Router en Next.js 14 marca un cambio de paradigma respecto al Pages Router histórico. Para un comparador de viajes, donde la estructura de las páginas es jerárquica (Inicio > Destino > Transporte > Detalle), el sistema de enrutamiento basado en el sistema de archivos del App Router ofrece una claridad indispensable.

Server Components por Defecto

Una de las ventajas mayores es el uso de React Server Components (RSC) por defecto. En nuestro comparador, las listas de resultados de búsqueda (trenes, autobuses, vuelos) se renderizan del lado del servidor. Esto significa que el código de recuperación de datos se ejecuta directamente en el servidor, cerca de la base de datos o de las API de terceros (SNCF, DB, Eurail). Esto elimina la necesidad de un estado de carga visible por el usuario para el contenido inicial y mejora el Largest Contentful Paint (LCP), una métrica clave de los Core Web Vitals definida por Google.

A diferencia de los enfoques anteriores donde el cliente debía fetchear los datos vía useEffect, ahora utilizamos componentes asíncronos directamente en la jerarquía de rutas. Aquí hay un ejemplo simplificado de nuestra página de resultados:

// 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>Resultados para {searchParams.from} hacia {searchParams.to}</h1>
      <TripList trips={trips} />
    </main>
  );
}

Este enfoque reduce el paquete JavaScript enviado al cliente, ya que la lógica de fetch no está incluida en el código descargado por el navegador. Para un usuario en una red móvil de viaje, este ahorro de datos y tiempo de procesamiento es significativo.

Gestión de Layouts Anidados

La estructura de nuestra aplicación requiere layouts persistentes, como la barra de navegación y el footer, que no deben recargarse durante la navegación entre páginas de resultados. El App Router gestiona esto nativamente mediante el archivo layout.tsx. Además, podemos tener layouts específicos para ciertas secciones, como la sección "Nightjet" o "Eurail", permitiendo inyectar scripts o estilos específicos sin afectar al resto de la aplicación.

TypeScript Strict: Una Seguridad Indispensable

Usar TypeScript es una cosa, usarlo en modo strict es otra. En nuestro tsconfig.json, activamos todas las opciones de estrictitud. Esto incluye noImplicitAny, strictNullChecks, y strictFunctionTypes. Para un comparador que agrega datos provenientes de múltiples API heterogéneas, la seguridad de tipos es nuestra primera línea de defensa.

Configuración tsconfig.json

Aquí está la base de nuestra configuración de TypeScript, alineada con las recomendaciones de 2026 para proyectos 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"]
}

La opción strict: true fuerza al desarrollador a gestionar explícitamente los casos donde una variable podría ser undefined o null. En el contexto de los viajes, un precio puede ser nulo (oferta gratuita), una hora de salida puede faltar (trayecto incompleto). TypeScript nos obliga a gestionar estos casos explícitamente, evitando los errores clásicos de tipo Cannot read property of undefined.

Tipado de Variables de Entorno

Un error común en los proyectos Next.js es el uso no tipado de las variables de entorno. Utilizamos una biblioteca como t3-env o un esquema Zod personalizado para validar las variables al inicio del servidor. Esto garantiza que si una clave API para un proveedor de trenes (ej: API DB o SNCF) falta, la aplicación se niega a iniciar en lugar de fallar silenciosamente en producción.

// 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,
  },
});

Esta práctica, recomendada por la comunidad Vercel y los mantenedores de Next.js, asegura que nuestra configuración sea tan robusta como nuestro código.

Validación de Datos con Zod y Server Actions

En una arquitectura App Router, las Server Actions reemplazan a menudo las API Routes tradicionales para las mutaciones de datos (como la reserva o el guardado de una alerta de precio). Sin embargo, los argumentos pasados a una Server Action no están tipados por defecto durante la llamada del cliente. Aquí es donde la validación de esquema se vuelve crítica.

Utilizamos Zod para validar las entradas del usuario antes de cualquier procesamiento de negocio. Esto protege contra inyecciones y datos malformados. Por ejemplo, al buscar un trayecto, debemos asegurarnos de que las fechas sean válidas y que los códigos de estación existan.

Validación de datos y flujo del servidor

Ejemplo de Server Action Tipada

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

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

const SearchSchema = z.object({
  origin: z.string().length(3), // Código 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: 'Datos inválidos', details: result.error.flatten() };
  }

  // Procesamiento de negocio seguro
  const trips = await db.trip.findMany({
    where: {
      origin: result.data.origin,
      // ... otros filtros
    }
  });

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

Combinando TypeScript strict y Zod, obtenemos una validación estática (en la compilación) y dinámica (en la ejecución). Esto es lo que llamamos "type safety end-to-end". Según las mejores prácticas TypeScript 2026, esta doble validación es esencial para aplicaciones críticas.

Rendimiento y SEO: Los Desafíos del Comparador

Un comparador de viajes vive de su posicionamiento orgánico (SEO). Los usuarios buscan "Tren París Berlín" o "Nightjet Bruselas". Next.js 14 destaca en este ámbito gracias al renderizado del lado del servidor estático (Static Site Generation) y al renderizado dinámico híbrido.

Optimización de Imágenes y Fuentes

Utilizamos el componente next/image para optimizar automáticamente los visuales de los trenes y los destinos. Esto permite servir formatos modernos como WebP o AVIF según el navegador del usuario. Además, el uso de next/font para cargar las fuentes (como Inter o Roboto) elimina el layout shift (CLS), garantizando que el texto no se mueva durante la carga.

Streaming y Suspense

Para las páginas de resultados complejas, utilizamos React Suspense. Esto permite mostrar inmediatamente el esqueleto de la página (header, filtros) mientras los resultados de búsqueda, que pueden tardar unos segundos en agregarse desde varios proveedores (Ouigo, TGV Inoui, DB), cargan en segundo plano. El usuario puede comenzar a interactuar con los filtros antes incluso de que la lista completa de trenes se muestre.

Esta técnica mejora considerablemente el Time to Interactive (TTI). En un contexto competitivo donde cada segundo cuenta para la conversión, esta optimización técnica se traduce directamente en un aumento de la tasa de reserva.

Internacionalización (i18n) y Mantenimiento

Nuestro comparador apunta a un público europeo. La gestión de varios idiomas (francés, alemán, inglés, español) es nativa en nuestra estructura de rutas. Next.js permite prefijar las rutas por la locale (/fr/search, /de/search).

Con TypeScript, tipamos las claves de traducción. Esto significa que si un desarrollador añade un nuevo texto en la interfaz pero olvida actualizar el archivo de traducción al alemán, el compilador levantará un error. Esto evita situaciones embarazosas donde una parte de la interfaz permanece en inglés en una página destinada al mercado alemán.

El mantenimiento del código también se facilita por la modularidad del App Router. Cada funcionalidad (búsqueda, pago, cuenta de usuario) está aislada en su propia carpeta con sus propias pruebas. Utilizamos Vitest para las pruebas unitarias y Playwright para las pruebas end-to-end, asegurando que las actualizaciones de dependencias (como la migración a React 19 prevista en el ecosistema Next.js) no rompan las funcionalidades existentes.

Conclusión: Una Base Sólida para el Futuro

La elección de Next.js 14 y TypeScript strict no es solo una tendencia técnica, es una necesidad operativa para un comparador de viajes en 2026. La complejidad de los datos ferroviarios y aéreos exige un rigor que el tipado estático impone naturalmente. El App Router ofrece la flexibilidad necesaria para componer interfaces ricas manteniendo un rendimiento de primer orden.

Siguiendo las recomendaciones de la documentación de Next.js y aplicando estándares estrictos de calidad de código, hemos construido una plataforma capaz de evolucionar. Ya sea para integrar nuevos proveedores como las líneas Nightjet extendidas o para soportar nuevas regulaciones europeas sobre datos de viajeros, nuestra arquitectura está lista. La técnica sirve aquí a la experiencia de usuario: un sitio más rápido, más fiable y más seguro para preparar tus próximos viajes a través de Europa.

Para ir más lejos, recomendamos consultar la documentación oficial de Next.js sobre el App Router y la guía de configuración de TypeScript para actualizaciones regulares sobre las mejores prácticas emergentes.

Rutas de tren populares

Reserva billetes de tren baratos y premium en las rutas más buscadas.