Inicio / Artículos

Crear una página de detalles de película resiliente con Next.js App Router

Obtener y almacenar en caché los datos de OMDB en el Next.js App Router utilizando componentes de servidor asíncronos, parámetros esperados y un manejo adecuado del error 404 para que la página de una película no falle.

Editor: REACTAPP.TOP7 min de lectura1556 palabras

Fuentes: A Movie Lookup Page in Next.js That Doesn’t Fall Over

Existen muchos tutoriales sobre cómo crear una página de búsqueda de películas con la API OMDB que siguen circulando y contienen código del año 2019: getServerSideProps, una importación de axios, un directorio pages/, y una función auxiliar fetch que simplemente envía todos los errores a console.error. Nada de eso funciona ya en una configuración moderna. Desde Next.js 16, la obtención de datos se realiza mediante componentes server-side asíncronos, y params ahora es una Promise que se debe esperar antes de usarla. Si se introduce ese código antiguo en un proyecto recién generado con create-next-app, el verificador de tipos se negará a funcionar.

Lo que sigue es la versión realmente útil para desplegar: una página dinámica de detalles de películas impulsada por la API OMDB, construida con el App Router, sin dependencia de axios, y una página 404 que se comporta realmente como tal.

La capa de datos

OMDB es una API REST sencilla. Envíale un título y recibirás JSON como respuesta. No hay ninguna biblioteca de cliente que valga la pena instalar, y axios no aporta nada aquí: fetch ya está integrado tanto en el navegador como en Node, y Next.js añade su propio sistema de caché sobre fetch de una manera en la que axios simplemente no puede participar.

Un detalle que engaña a muchos usuarios: OMDB responde con HTTP 200 incluso cuando no encuentra una película. El verdadero indicador de error se encuentra en el contenido de la respuesta, específicamente en "Response": "False". Si tu código solo verifica res.ok, un título escrito incorrectamente parecerá una llamada exitosa, y terminarás mostrando undefined en toda la página.

const BASE_URL = "https://www.omdbapi.com/";
export type Movie = {
  Title: string;
  Year: string;
  Plot: string;
  Poster: string;
  imdbRating: string;
  Response: "True" | "False";
  Error?: string;
};export async function fetchMovieByTitle(title: string): Promise<Movie | null> {
  const apiKey = process.env.OMDB_API_KEY;
  if (!apiKey) throw new Error("OMDB_API_KEY is not set");  const url = new URL(BASE_URL);
  url.searchParams.set("apikey", apiKey);
  url.searchParams.set("t", title);
  url.searchParams.set("plot", "full");  const res = await fetch(url, { next: { revalidate: 60 * 60 * 24 } });
  if (!res.ok) return null;  const data = (await res.json()) as Movie;
  return data.Response === "True" ? data : null;
}

La opción next: { revalidate: ... } en fetch es lo que realmente brinda escalabilidad a esta configuración. El resumen de la trama y el póster de una película son, en efecto, estáticos; por lo tanto, almacenar en caché la respuesta durante todo un día significa que la segunda persona que visite /movie/inception nunca generará otra llamada a OMDB. Esa es toda la estrategia de caché: no se necesita ninguna instancia de Redis ni tarea cron de revalidación. ¿Quiere datos más actualizados? Reduzca el número. ¿Quiere que nunca caduque? Cambie a cache: "force-cache". Diez búsquedas directas a OMDB tuvieron un promedio de 57.8 ms por solicitud (p50 de 55.5 ms, con un caso máximo de 112.8 ms); así que el propósito del caché no es solucionar la lentitud de su API, sino evitar incluso ese costo modesto en cada solicitud.

Construir la URL de la solicitud con URLSearchParams en lugar de interpolación manual de cadenas es más importante de lo que parece. Los títulos de las películas suelen contener espacios, signos y dos puntos. Un título como t: "The Good, the Bad and the Ugly" fallará si se concatena a mano, pero searchParams.set gestiona la codificación automáticamente.

La página

Aquí es donde los tutoriales antiguos no solo están desactualizados, sino que además son inoperativos. Bajo App Router, un componente de página es una función asíncrona, y params aparece como un Promise que hay que esperar. Si se omite ese paso, TypeScript no solo emitirá una advertencia; la compilación con next build fallará por completo.

import Image from "next/image";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { fetchMovieByTitle } from "@/lib/omdb";
type PageProps = { params: Promise<{ title: string }> };export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const { title } = await params;
  const movie = await fetchMovieByTitle(decodeURIComponent(title));
  if (!movie) return { title: "Movie not found" };
  return { title: `${movie.Title} (${movie.Year})`, description: movie.Plot };
}export default async function MoviePage({ params }: PageProps) {
  const { title } = await params;
  const movie = await fetchMovieByTitle(decodeURIComponent(title));
  if (!movie) notFound();  return (
    <main>
      <h1>
        {movie.Title} <span>({movie.Year})</span>
      </h1>
      <p>IMDb {movie.imdbRating}</p>
      <p>{movie.Plot}</p>
      {movie.Poster !== "N/A" && (
        <Image
          src={movie.Poster}
          alt={`Poster for ${movie.Title}`}
          width={300}
          height={445}
          priority
        />
      )}
    </main>
  );
}

La llamada a notFound() realiza una tarea útil aquí. En lugar de renderizar un elemento JSX como `

Película no encontrada

mientras sigue devolviendo un código de estado 200 — que es exactamente lo que hacen las versiones antiguas, indicando efectivamente a Google “esta página es real, proceda a indexarla” —notFound()lanza una excepción, muestra tu archivonot-found.tsx` y devuelve un código de estado 404 real. Dado que la mayoría de los títulos adivinados en una búsqueda basada en URL como esta no resolverán a nada, esa distinción es lo que evita que tu sitio tenga un montón de páginas innecesarias en el índice de búsqueda.

La función generateMetadata utiliza exactamente el mismo helper para obtener datos que la página en sí. Podría pensarse que esto duplicaría el número de llamadas a la API, pero no es así: Next.js elimina automáticamente las solicitudes fetch idénticas que ocurren durante una sola pasada de renderizado, por lo que el generador de metadatos y el componente de página terminan compartiendo una sola llamada a la red. Este es un argumento sólido a favor de mantener la lógica de obtención de datos dentro de los Components Server en lugar de trasladarla a un useEffect del lado del cliente, ya que una versión renderizada en el cliente no puede eliminar duplicados más allá de esos límites de metadatos.

Existen dos peculiaridades de OMDB relacionadas específicamente con el manejo de pósteres que son fáciles de pasar por alto. Primero, cuando falta un póster, se devuelve como la cadena literal "N/A" en lugar de null; por lo tanto, es necesario realizar una verificación como movie.Poster !== "N/A" antes de pasar el valor a next/image. Si se proporciona directamente "N/A" como valor de src, se obtendrá una imagen corrupta además de una advertencia en la consola. Segundo, next/image se niega a cargar imágenes desde un dominio remoto que no esté explícitamente incluido en la lista de permisos, por lo que el servidor de imágenes de OMDB necesita tener su propia entrada en next.config:

const nextConfig = {
  images: { remotePatterns: [{ protocol: "https", hostname: "m.media-amazon.com" }] },
};
export default nextConfig;

Qué aún no hace esto

No hay cuadro de búsqueda en esta implementación: se obtiene un título exacto a partir de la URL, y eso es intencional, ya que el parámetro t= de OMDB realiza una búsqueda exacta, mientras que su búsqueda difusa s= se encuentra en un endpoint separado y menos predecible. Tampoco existe una capa de persistencia; cada consulta única sigue llegando a OMDB, solo detrás del caché. Si su proyecto necesita funciones como una lista de favoritos o un historial de visionado, eso requiere una base de datos real, lo cual es un tema aparte que está fuera del alcance de esta página.

Lo primero que merece atención antes de pasar a la producción es el límite del plan gratuito de OMDB, que permite 1,000 solicitudes por día. La revalidación diaria amplía considerablemente ese límite, pero una vez que el tráfico supere los niveles de un proyecto aficionado, será necesario contar con su propio nivel de almacenamiento delante de OMDB: un caché de lectura que solo se actualiza cuando no se encuentra la información. Todo lo descrito anteriormente constituye esencialmente la primera mitad de dicha arquitectura.

La conclusión es que “escalable” en este contexto no se refiere a algún framework que se añada posteriormente. Se trata de establecer correctamente el valor de revalidate, devolver un 404 adecuado y asegurarse de no llamar a la API dos veces cuando una sola llamada es suficiente.

Lecturas relacionadas