Tworzenie odpornej strony szczegółowej filmu za pomocą Next.js App Router
Pobieraj i przechowuj dane z OMDB w Next.js App Router za pomocą asynchronicznych komponentów serwerowych, parametrów awaited oraz prawdziwego obsługi błędu 404 dla strony filmowej, która nie przestaje działać.
Źródła: A Movie Lookup Page in Next.js That Doesn’t Fall Over
Bardzo wiele tutoriali na temat tworzenia strony wyszukiwania filmów przy użyciu API OMDB wciąż krąży z kodem z 2019 roku: funkcja getServerSideProps, import biblioteki axios, katalog pages/ oraz pomocnik fetch, który po prostu wysyła każdy błąd do console.error. Nic z tego już nie działa w nowoczesnych środowiskach. Od wersji Next.js 16 pobieranie danych odbywa się w ramach asynchronicznych komponentów serwerowych, a params to teraz obiekt typu Promise, którego należy odczekać przed użyciem. Gdy wkleisz ten stary kod do świeżo utworzonego projektu z narzędzia create-next-app, narzędzie do sprawdzania typów odmówi współpracy.
Poniżej znajduje się wersja nadająca się do rzeczywistego użycia: dynamiczna strona z informacjami o filmie działająca dzięki API OMDB, stworzona z wykorzystaniem App Routera, bez zależności od axios oraz strona 404, która rzeczywiście zachowuje się jak typowa strona 404.
Szczegóły danych
OMDB to prosty API REST. Wysyłasz do niego tytuł filmu i otrzymujesz odpowiedź w formacie JSON. Nie ma potrzeby instalowania żadnej biblioteki klienta, a axios nie dodaje tu nic nowego — fetch jest już wbudowany zarówno w przeglądarkę, jak i w Node, a Next.js dodaje własne mechanizmy cacheowania na bazie fetch w sposób, w jaki axios po prostu nie może uczestniczyć.
Detale, które mylą użytkowników: OMDB zwraca kod HTTP 200 nawet wtedy, gdy nie udaje mu się znaleźć filmu. Prawdziwy wskaźnik błędu znajduje się w treści odpowiedzi, konkretnie w wartości "Response": "False". Jeśli twój kod sprawdza tylko res.ok, błędnie wpisany tytuł będzie wyglądał jak udana próba połączenia, co skutkuje wyświetlaniem wartości undefined na całej stronie.
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;
}
Opcja next: { revalidate: ... } w funkcji fetch jest tym, co naprawdę zapewnia tej konfiguracji skalowalność. Streszczenie fabuły i plakat filmu są w praktyce statyczne, więc przechowywanie odpowiedzi przez cały dzień oznacza, że druga osoba odwiedzająca adres /movie/inception w ogóle nie wywoła kolejnego żądania do OMDB. To właśnie cała strategia cacheowania – nie potrzeba instancji Redis ani zadania cron do ponownej weryfikacji danych. Chcesz świeższych danych? Zmniejsz liczbę. Chcesz, aby dane nigdy nie wygasły? Użyj opcji cache: "force-cache". Te bezpośrednie zapytania do OMDB dawały średnio 57,8 ms na żądanie (p50 wynosił 55,5 ms, a najgorszy przypadek 112,8 ms), więc sens cacheowania nie polega na tym, że ich API jest wolne – chodzi o uniknięcie nawet tak niewielkich kosztów przy każdym żądaniu.
Budowanie adresu URL żądania za pomocą URLSearchParams zamiast ręcznego łączenia ciągów znaków ma większe znaczenie, niż się wydaje. Tytuły filmów często zawierają spacje, znaki ampersand oraz kropki. Tytuł taki jak t: "The Good, the Bad and the Ugly" nie będzie działał poprawnie, jeśli połączysz go ręcznie, natomiast searchParams.set automatycznie zajmuje się kodowaniem.
Strona
Tutaj starsze tutoriale są nie tylko przestarzałe — są wręcz błędne. W App Routerze komponent strony to funkcja asynchroniczna, a params występuje jako Promise, który trzeba odczekać. Jeśli pominiesz ten krok, TypeScript nie tylko wyda ostrzeżenie; proces next build całkowicie się nie powiedzie.
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>
);
}
Wywołanie notFound() pełni tu istotną funkcję. Zamiast wyświetlać element JSX taki jak `
Film nie został znaleziony
choć nadal zwraca kod stanu 200 — co dokładnie robią starsze wersje, faktycznie mówiąc do Google „ta strona istnieje, możesz ją indeksować” —metodanotFound()wyrzuca wyjątek, renderuje pliknot-found.tsx` i zwraca rzeczywisty kod stanu 404. Biorąc pod uwagę, że większość domyślnych tytułów w takim wyszukiwaniu opartym na adresach URL nie przypisze się do żadnej konkretnej strony, to właśnie ta różnica zapobiega pojawieniu się w indeksie wyszukiwarki mnóstwa niepotrzebnych stron.
Funkcja generateMetadata wykorzystuje dokładnie ten sam pomocnik do pobierania danych co sama strona. Można by oczekiwać, że to podwoi liczbę wywołań API, ale tak nie jest: Next.js automatycznie eliminuje duplikaty identycznych żądań fetch, które pojawiają się podczas jednej rundy renderowania, dzięki czemu generator metadanych i komponent strony korzystają z jednego wywołania sieciowego. To mocny argument za umieszczaniem logiki pobierania danych w komponentach serwerowych zamiast przenoszenia jej do useEffect po stronie klienta — wersja renderowana po stronie klienta nie ma możliwości eliminowania duplikatów ponad granicami tych metadanych.
Istnieją dwa specyficzne problemy z OMDB związane z obsługą plakatów, które łatwo przeoczyć. Po pierwsze, brakujący plakat jest zwracany jako dosłowny ciąg znaków "N/A" zamiast null, więc należy sprawdzić to za pomocą warunku movie.Poster !== "N/A" przed przekazaniem wartości do next/image — jeśli podasz mu bezpośrednio "N/A" jako src, otrzymasz uszkodzony obraz wraz z ostrzeżeniem w konsoli. Po drugie, next/image odmawia ładowania obrazów z zdalnego domeny, która nie została wyraźnie dodana do listy dozwolonych, więc host obrazów OMDB musi mieć własną pozycję w next.config:
const nextConfig = {
images: { remotePatterns: [{ protocol: "https", hostname: "m.media-amazon.com" }] },
};
export default nextConfig;
Czego to jeszcze nie robi
W tej implementacji nie ma pola wyszukiwania — rozwiązuje ono problem na podstawie dokładnego tytułu z adresu URL, i to jest zamierzone, ponieważ parametr t= w OMDB realizuje dokładne dopasowanie, natomiast jego nieprecyzyjne wyszukiwanie za pomocą s= odbywa się w oddzielnym, mniej przewidywalnym punkcie dostępu. Nie ma również warstwy persistencji; każde unikalne zapytanie nadal trafia do OMDB, tuż za buforem pamięci. Jeśli Twój projekt wymaga funkcji takich jak lista ulubionych lub historia oglądanych filmów, to wymaga rzeczywistej bazy danych, co stanowi odrębną kwestię poza zakresem tej strony.
Pierwszą kwestią, którą należy rozważyć przed wdrożeniem w produkcji, jest ograniczenie darmowej wersji OMDB wynoszące 1000 żądań dziennie. Codzienne ponowne walidowanie znacznie rozszerza tę kwotę, ale gdy ruch przekroczy poziom projektu hobbystycznego, konieczne będzie stworzenie własnej warstwy przechowywania przed OMDB – pamięci cache, która jest aktualizowana tylko w przypadku nieudanego dostępu. Wszystko to, co opisano powyżej, stanowi zasadniczo pierwszą połowę tej architektury.
Główny wniosek jest taki, że „skalowalność” w tym kontekście to nie jakaś dodatkowa ramka, którą instaluje się później. Chodzi o prawidłowe ustawienie wartości revalidate, zwracanie odpowiedniego kodu 404 oraz upewnienie się, że nigdy nie wywołuje się API dwa razy, gdy wystarczy jeden wywołanie.
Literatura pokrewna
- Lekcje z wyślij Next.js SaaS i stworzenia CLI do budowy — Wyjaśnia podstawowe decyzje dotyczące wyboru technologii, autoryzacji, modelu multi-tenancy, systemów rozliczeń oraz zarządzania stanem — a także proces tworzenia CLI, które ułatwia konfigurację projektów Next.js.
- Tworzenie interfejsów użytkownika dla agentów AI wieloetapowych z Next.js i AI SDK — Dowiedz się, jak zaprojektować gotowy do użycia interfejs agenta AI przy użyciu narzędzi typowanych, pętli wieloetapowych oraz komponentów interfejsu generatywnego w formie strumieniowej w Next.js.