Créer une page de détail de film résiliente avec le Next.js App Router
Récupérer et mettre en cache les données OMDB dans le Next.js App Router à l’aide de composants serveur asynchrones, de paramètres attendus et d’un traitement réel des erreurs 404 pour une page de film qui ne plante pas.
Sources: A Movie Lookup Page in Next.js That Doesn’t Fall Over
De nombreux tutoriels sur la création d’une page de recherche de films à l’aide de l’API OMDB circulent encore, utilisant du code datant de 2019 : getServerSideProps, une importation d’axios, un répertoire pages/, ainsi qu’un outil fetch qui envoie simplement chaque erreur dans console.error. Rien de tout cela ne fonctionne plus avec une configuration moderne. Depuis Next.js 16, la récupération de données s’effectue au sein des composants serveur asynchrones, et params est désormais une Promise que l’on doit attendre avant de l’utiliser. Si l’on insère ce code ancien dans un projet généré récemment avec create-next-app, le vérificateur de types refusera de fonctionner.
Ce qui suit est la version vraiment utilisable : une page dynamique d’informations sur les films alimentée par l’API OMDB, construite avec le App Router, sans dépendance axios, ainsi qu’une page 404 qui se comporte réellement comme telle.
La couche de données
OMDB est une API REST simple. Envoyez-lui un titre et vous obtiendrez du JSON en retour. Il n’y a pas de bibliothèque client à installer, et axios ne sert à rien ici — fetch est désormais intégré tant dans le navigateur que dans Node, et Next.js ajoute son propre mécanisme de mise en cache par-dessus fetch d’une manière à laquelle axios ne peut pas participer.
Détail qui prête souvent à confusion : OMDB renvoie un code HTTP 200 même lorsqu’il ne parvient pas à trouver un film. Le véritable indicateur d’échec se trouve dans le contenu du message, sous la forme de "Response": "False". Si votre code ne vérifie que res.ok, un titre mal orthographié sera perçu comme une requête réussie, ce qui entraînera l’affichage de undefined dans toute la page.
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;
}
L’option next: { revalidate: ... } dans fetch est ce qui confère réellement une grande scalabilité à cette configuration. Le résumé de l’intrigue et l’affiche d’un film étant essentiellement statiques, en mémorisant la réponse pendant toute une journée, la deuxième personne qui visite /movie/inception n’a absolument pas besoin de lancer une nouvelle requête vers OMDB. Telle est toute la stratégie de mise en cache : aucune instance Redis ni tâche cron de révalidation requise. Souhaitez-vous des données plus récentes ? Diminuez ce chiffre. Voulez-vous qu’elles ne expirent jamais ? Utilisez cache: "force-cache". Dix requêtes directes vers OMDB ont donné en moyenne 57,8 ms par demande (p50 à 55,5 ms, avec un cas extrême de 112,8 ms) ; donc l’avantage de la mise en cache n’est pas tant que leur API soit lente, mais plutôt d’éviter de payer même ce coût modeste à chaque demande.
La construction de l’URL de la requête à l’aide de URLSearchParams plutôt que par interpolation manuelle de chaînes est plus importante qu’il n’y paraît. Les titres de films contiennent fréquemment des espaces, des ampersands et des deux-points. Un titre comme t: "The Good, the Bad and the Ugly" ne fonctionnera pas si vous le concaténez manuellement, mais searchParams.set gère automatiquement l’encodage.
La page
C’est ici que les tutoriels plus anciens ne sont pas seulement obsolètes — ils sont même défectueux. Avec App Router, un composant de page est une fonction asynchrone, et params apparaît sous forme de Promise que vous devez attendre. Si vous sautez cette étape, TypeScript ne se contentera pas de vous avertir ; la compilation échouera complètement.
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 mise en œuvre de notFound() effectue ici un travail utile. Au lieu d’afficher un élément JSX comme `
Le film n’a pas été trouvé
tandis qu’il renvoie toujours un code d’état 200 — ce que font précisément les versions anciennes, en indiquant ainsi à Google « cette page est réelle, continuez à l’indexer » —notFound()provoque une exception, affiche votre fichiernot-found.tsx` et renvoie un véritable code d’état 404. Étant donné que la plupart des titres supposés dans une recherche basée sur l’URL ne mènent à rien, c’est cette distinction qui empêche votre site d’accumuler un tas de pages inutiles dans l’index de recherche.
La fonction generateMetadata utilise exactement le même outil de récupération de données que la page elle-même. On pourrait penser que cela doublerait le nombre d’appels API, mais ce n’est pas le cas : Next.js supprime automatiquement les requêtes fetch identiques qui ont lieu au cours d’une même passe de rendu, de sorte que le générateur de métadonnées et le composant de page partagent finalement un seul appel réseau. C’est un argument solide en faveur du fait de conserver la logique de récupération des données à l’intérieur des composants serveur plutôt que de la déplacer dans un useEffect côté client — une version rendue côté client ne peut pas supprimer les doublons au-delà de cette frontière des métadonnées.
Il existe deux particularités d’OMDB liées spécifiquement à la gestion des affiches qui sont facilement négligées. Premièrement, en l’absence d’affiche, la valeur retournée est la chaîne littérale "N/A" plutôt que null ; il est donc nécessaire de vérifier avec movie.Poster !== "N/A" avant d’envoyer cette valeur à next/image. Si vous lui fournissez directement "N/A" comme valeur src, l’image ne s’affichera pas correctement et un avertissement apparaîtra dans la console. Deuxièmement, next/image refuse de charger des images provenant d’un domaine distant qui n’a pas été explicitement ajouté à la liste autorisée ; par conséquent, l’hôte d’images d’OMDB doit disposer de sa propre entrée dans next.config:
const nextConfig = {
images: { remotePatterns: [{ protocol: "https", hostname: "m.media-amazon.com" }] },
};
export default nextConfig;
Ce que cela ne fait pas encore
Il n’y a pas de champ de recherche dans cette implémentation — elle extrait un titre exact à partir de l’URL, et c’est intentionnel, car le paramètre t= d’OMDB effectue une recherche par correspondance exacte, tandis que sa recherche floue s= fonctionne via une interface distincte et moins prévisible. Il n’y a pas non plus de couche de persistance ; chaque recherche unique atteint toujours OMDB, juste derrière le cache. Si votre projet a besoin de fonctionnalités telles qu’une liste de favoris ou un historique des visionnages, il faut alors recourir à une véritable base de données, ce qui relève d’une question distincte en dehors du champ d’application de cette page.
La première chose à prendre en compte avant de passer en production est la limite du plan gratuit d’OMDB, qui autorise 1 000 requêtes par jour. La révalidation quotidienne permet d’augmenter considérablement cette limite, mais une fois le trafic dépassant celui d’un projet amateur, il sera nécessaire de disposer d’une propre couche de stockage en amont d’OMDB — un cache de lecture qui ne s’alimente que en cas d’échec de recherche. Tout ce qui a été décrit ci-dessus constitue essentiellement la première moitié de cette architecture.
En résumé, le terme « scalable » dans ce contexte ne désigne pas un framework à ajouter ultérieurement. Il s’agit plutôt de définir correctement la valeur de revalidate, de renvoyer un code 404 approprié, et de s’assurer de ne jamais appeler l’API deux fois lorsque une seule appel suffit.