Erstellung einer widerstandsfähigen Film-Detailseite mit dem Next.js App Router
Laden und Cachen von OMDB-Daten im Next.js App Router mithilfe von asynchronen Server Components, awaited Parametern sowie einer echten 404-Behandlung, damit die Filmseite nicht fehlerhaft funktioniert.
Quellen: A Movie Lookup Page in Next.js That Doesn’t Fall Over
Viele Tutorials zur Erstellung einer Film-Suchseite mit der OMDB API verbreiten sich immer noch und enthalten Code aus dem Jahr 2019: getServerSideProps, eine axios-Import-Funktion, ein pages/-Verzeichnis sowie ein Fetch-Hilfsprogramm, das alle Fehler einfach in console.error schreibt. Nichts davon funktioniert mehr in einer modernen Umgebung. Seit Next.js 16 findet das Datenabrufen in asynchronen Server Components statt, und params ist nun ein Promise, das vor der Verwendung abgewartet werden muss. Wenn man diesen alten Code in ein frisch erstelltes create-next-app-Projekt einfügt, weigert sich der Typprüfer, damit zu arbeiten.
Was folgt, ist die Version, die tatsächlich veröffentlicht werden kann: eine dynamische Film-Detailseite, die von der OMDB API angetrieben wird, mit dem App Router erstellt wurde, keine axios-Abhängigkeit aufweist und eine 404-Seite besitzt, die sich wirklich wie eine 404-Seite verhält.
Die Datenschicht
OMDB ist eine einfache REST API. Senden Sie einen Titel, erhalten Sie JSON als Antwort. Es gibt keine Client-Bibliothek, die man installieren müsste, und axios bringt hier nichts hinzu – fetch ist bereits in Browsern und Node integriert, wobei Next.js seine eigene Caching-Funktion auf fetch aufbaut, an der axios einfach nicht teilnehmen kann.
Ein Detail, das viele überrascht: OMDB gibt auch dann HTTP 200 zurück, wenn ein Film nicht gefunden werden kann. Der eigentliche Fehlerindikator befindet sich im Payload, genauer gesagt in "Response": "False". Wenn Ihr Code nur res.ok überprüft, erscheint ein falsch geschriebener Titel wie eine erfolgreiche Anfrage, wodurch im gesamten Seiteninhalt undefined angezeigt wird.
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;
}
Die Option next: { revalidate: ... } bei fetch sorgt tatsächlich für die nötige Skalierbarkeit dieser Konfiguration. Die Zusammenfassung und das Poster eines Films sind im Grunde statisch, wodurch das Caching der Antwort für einen ganzen Tag bedeutet, dass die zweite Person, die /movie/inception aufsucht, überhaupt keinen weiteren OMDB-Aufruf auslöst. Das ist die gesamte Caching-Strategie – es wird weder eine Redis-Instanz noch ein Cron-Job zur Neuvalidierung benötigt. Möchten Sie aktuellere Daten? Verringern Sie die Zahl. Möchten Sie, dass die Daten niemals ablaufen? Wechseln Sie auf cache: „force-cache“. Zehn direkte Abfragen bei OMDB ergaben im Durchschnitt 57,8 ms pro Anfrage (p50 bei 55,5 ms, mit einem Schlimmstfall von 112,8 ms); der Sinn des Cachings liegt also nicht darin, dass ihre API langsam ist – sondern darin, diesen bereits geringen Aufwand bei jeder einzelnen Anfrage zu vermeiden.
Die Erstellung der Anfragen-URL mit URLSearchParams anstelle manueller Zeichenketteninterpolation ist wichtiger, als es scheint. Filmtitel enthalten häufig Leerzeichen, Ampersand-Zeichen und Kolons. Ein Titel wie t: "The Good, the Bad and the Ugly" funktioniert nicht mehr, wenn man ihn manuell zusammenfügt, doch searchParams.set kümmert sich automatisch um die Kodierung.
Die Seite
Hier sind ältere Tutorials nicht nur veraltet – sie funktionieren sogar gar nicht mehr. Unter App Router ist ein Seitenkomponente eine asynchrone Funktion, und params erscheint als Promise, den man abwarten muss. Überspringt man diesen Schritt, warnt TypeScript nicht nur – die next build fehlschlägt völlig.
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>
);
}
Der Aufruf von notFound() erledigt hier eine sinnvolle Aufgabe. Anstelle der Darstellung eines JSX-Elements wie `
Movie not found
`während weiterhin der Statuscode 200 zurückgegeben wird – genau wie es die älteren Versionen tun und Google somit signalisieren „Diese Seite existiert, indexiere sie einfach“ –wird bei notFound() eine Ausnahme ausgelöst, Ihre Datei not-found.tsx angezeigt und tatsächlich der Status 404 zurückgegeben. Da die meisten in einer solchen URL-basierten Suche angenommenen Titel zu nichts führen, verhindert genau diese Unterscheidung, dass auf Ihrer Website eine Menge unnützer Seiten im Suchindex landen.
Die Funktion generateMetadata ruft genau denselben Hilfsfunktion zur Datenerfassung auf wie die Seite selbst. Man könnte erwarten, dass dies die Anzahl der API-Aufrufe verdoppelt, doch das ist nicht der Fall: Next.js entfernt automatisch identische fetch-Anfragen, die innerhalb einer einzigen Render-Schleife auftreten, sodass der Metadaten-Generator und die Seitekomponente letztendlich nur einen Netzwerkaufruf teilen. Dies ist ein überzeugender Grund, die Logik zur Datenerfassung in Server Components zu belassen und sie nicht in einen client-seitigen useEffect zu verlegen – eine auf der Client-Seite gerenderte Version hat keine Möglichkeit, solche Duplikate jenseits dieser Metadaten-Grenze zu beseitigen.
Es gibt zwei Besonderheiten bei OMDB, die sich speziell auf das Handhaben von Plakaten beziehen und leicht übersehen werden können. Erstens wird ein fehlendes Plakat als die exakte Zeichenkette "N/A" zurückgegeben anstelle von null; daher ist eine Überprüfung wie movie.Poster !== "N/A" erforderlich, bevor der Wert an next/image übergeben wird. Wenn man "N/A" direkt als src übermittelt, erhält man ein fehlerhaftes Bild zusammen mit einer Warnung in der Konsole. Zweitens weigert sich next/image, Bilder von einer entfernten Domain zu laden, die nicht ausdrücklich in der Allow-Liste aufgeführt ist; daher benötigt OMDBs Bildhost einen eigenen Eintrag in next.config:
const nextConfig = {
images: { remotePatterns: [{ protocol: "https", hostname: "m.media-amazon.com" }] },
};
export default nextConfig;
Was das bisher noch nicht kann
In dieser Implementierung gibt es kein Suchfeld – sie löst nur einen exakt passenden Titel aus der URL, und das ist beabsichtigt, denn OMDBs t=-Parameter führt eine exakte Abgleichsuche durch, während die ungenaue s=-Suchfunktion über einen separaten, weniger vorhersehbaren Endpunkt verfügt. Es gibt außerdem keine Persistenzschicht; jede eindeutige Abfrage erreicht weiterhin OMDB, nur hinter dem Cache. Wenn Ihr Projekt Funktionen wie eine Favoritenliste oder einen Anschauungshistorien benötigt, ist ein eigentlicher Datenbanksystem erforderlich – das gehört zu einem anderen Thema außerhalb des Umfangs dieser Seite.
Das Erste, was vor dem Einsatz in der Produktion berücksichtigt werden muss, ist die Begrenzung der kostenlosen Stufe von OMDB auf 1.000 Anfragen pro Tag. Die tägliche Neuvalidierung dehnt diese Quote erheblich aus, doch sobald der Datenverkehr das Niveau eines Hobbyprojekts überschreitet, möchten Sie eine eigene Speicherschicht vor OMDB haben – einen Durchlesungsspeicher, der nur bei Fehlern gefüllt wird. Alles, was oben beschrieben wurde, stellt im Grunde die erste Hälfte dieser Architektur dar.
Fazit: „Skalierbar“ bedeutet in diesem Kontext nicht irgendein Framework, das man später hinzufügt. Es geht darum, einen revalidate-Wert richtig einzustellen, eine ordnungsgemäße 404-Antwort zurückzugeben und sicherzustellen, dass man die API nie zweimal aufruft, wenn eine einzige Anfrage ausreicht.