Создание устойчивой страницы с деталями фильма с использованием Next.js App Router
Загружайте и храните в кэше данные OMDB в Next.js App Router с использованием асинхронных серверных компонентов, оператора awaited для параметров и реальной обработки ошибки 404 для страницы фильма, чтобы она не выходила из строя.
Источники: A Movie Lookup Page in Next.js That Doesn’t Fall Over
В сети по-прежнему циркулируют многочисленные учебники по созданию страницы поиска фильмов с использованием API OMDB, в которых приводится код 2019 года: функция getServerSideProps, импорт библиотеки axios, каталог pages/, а также вспомогательная функция fetch, которая просто выводит все ошибки в console.error. Ничто из этого больше не работает в современных средах. Начиная с Next.js 16, загрузка данных происходит в асинхронных серверных компонентах, а параметры params теперь представляют собой объект Promise, который необходимо обработать с помощью функции await перед использованием. Если попытаться вставить такой старый код в только что созданный проект с помощью команды create-next-app, проверка типов откажется работать.
Ниже приведена версия, годная к реальному использованию: динамическая страница с информацией о фильме, работающая через API OMDB, созданная с использованием App Router без зависимости от axios, а также страница 404, которая действительно ведет себя как настоящая страница 404.
Слой данных
OMDB — это простой REST API. Отправьте ему название фильма, и вы получите JSON-ответ. Нет необходимости использовать какие-либо клиентские библиотеки, а axios здесь совершенно не нужен — функция fetch встроена как в браузер, так и в Node, а Next.js добавляет собственную систему кэширования поверх fetch, в которой axios просто не может участвовать.
Есть деталь, которая вводит людей в заблуждение: OMDB возвращает код HTTP 200 даже тогда, когда не может найти фильм. Настоящий индикатор ошибки находится в теле ответа, а именно в значении "Response": "False". Если ваш код проверяет только параметр res.ok, то неправильно написанное название будет восприниматься как успешный запрос, и в результате по всей странице будут отображаться значения undefined.
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;
}
Параметр next: { revalidate: ... } в функции fetch обеспечивает настоящую масштабируемость данной схемы. Краткое описание сюжета фильма и его постер фактически являются статическими, поэтому хранение ответа в кэше на целый день гарантирует, что второй посетитель страницы /movie/inception совсем не будет выполнять дополнительный запрос к OMDB. Вся стратегия кэширования заключается именно в этом — не требуется ни инстанция Redis, ни задача на перепроверку данных через cron. Хотите более свежие данные? Уменьшите значение этого параметра. Хотите, чтобы кэш никогда не истекал? Используйте параметр cache: "force-cache". Десять прямых запросов к OMDB давали в среднем 57,8 мс на запрос (50-й процентиль — 55,5 мс, а худший случай — 112,8 мс), поэтому смысл кэширования заключается не в том, что их API медленный, а в том, чтобы избежать уплаты даже такой небольшой суммы за каждый запрос.
Создание URL запроса с использованием URLSearchParams вместо ручного вставления строк имеет большее значение, чем кажется. Названия фильмов часто содержат пробелы, знаки & и двоеточия. Например, название вида t: "The Good, the Bad and the Ugly" сломается, если его объединять вручную, но метод searchParams.set автоматически обрабатывает кодировку.
Страница
Именно здесь старые учебные материалы не просто устарели — они на самом деле некорректны. В рамках App Router компонент страницы представляет собой асинхронную функцию, а переменная params выступает в роли объекта Promise, который необходимо обработать с помощью метода await. Если пропустить этот шаг, TypeScript не только выдаст предупреждение — сборка приложения (next build) полностью провалится.
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>
);
}
Здесь вызов метода notFound() выполняет важную функцию. Вместо отображения элемента JSX вроде
Фильм не найден
хотя по-прежнему возвращает код статуса 200 — что и делают более старые версии, фактически сообщая Google: «эта страница реальна, можете её индексировать» —методnotFound()вызывает исключение, отображает ваш файлnot-found.tsx` и возвращает настоящий код статуса 404. Учитывая, что большинство предполагаемых названий в поиске по URL не приведут ни к чему, именно это различие предотвращает появление кучи бесполезных страниц в индексе поиска.
Функция generateMetadata использует точно тот же самый инструмент для получения данных, что и сама страница. Можно было бы ожидать, что это удвоит количество запросов к API, но этого не происходит: Next.js автоматически устраняет дубликаты одинаковых запросов fetch, совершаемых в ходе одной процедуры отрисовки, поэтому генератор метаданных и компонент страницы в итоге делит один сетевой запрос. Это убедительный аргумент в пользу того, чтобы логику получения данных оставлять внутри серверных компонентов, а не переносить её в клиентский useEffect — версия, отрисовываемая на стороне клиента, не может устранять дубликаты за пределами этого метаданных контекста.
Существуют две особенности OMDB, связанные непосредственно с обработкой постеров, которые легко упустить. Во-первых, отсутствующий постер возвращается в виде строгой строки "N/A", а не null; поэтому перед передачей значения в next/image необходимо выполнить проверку вида movie.Poster !== "N/A" — если передать значение "N/A" непосредственно в качестве параметра src, изображение не отобразится, и появится предупреждение в консоли. Во-вторых, next/image отказывается загружать изображения с удаленного домена, который не был явно добавлен в список разрешенных, поэтому хост изображений OMDB требует отдельной записи в файле next.config:
const nextConfig = {
images: { remotePatterns: [{ protocol: "https", hostname: "m.media-amazon.com" }] },
};
export default nextConfig;
Чего это пока не делает
В этой реализации отсутствует поле поиска — она определяет только один точный заголовок из URL, и это сделано намеренно, поскольку параметр t= в OMDB обеспечивает точное совпадение, тогда как нечеткий поиск с помощью параметра s= работает через отдельный, менее предсказуемый эндпоинт. Также отсутствует слой сохранения данных; каждый уникальный запрос все равно направляется в OMDB, просто после кэша. Если ваш проект требует таких функций, как список любимых фильмов или история просмотров, это требует настоящей базы данных, и это отдельная проблема, выходящая за рамки данной страницы.
Первым моментом, который стоит рассмотреть перед запуском в производственную среду, является ограничение бесплатного тарифа OMDB — 1 000 запросов в день. Ежедневная переподтверждение данных значительно увеличивает этот лимит, но как только объем трафика превысит уровень хобби-проекта, вам понадобится собственный слой хранения перед OMDB — кэш для чтения, который заполняется только при неудачном поиске. Все описанное выше представляет собой, по сути, первую часть такой архитектуры.
Главный вывод заключается в том, что понятие «масштабируемость» в данном контексте — это не какая-то дополнительная библиотека, которую добавляют позже. Оно сводится к правильному установлению значения параметра revalidate, возврату корректного кода 404 и обеспечению того, чтобы API не вызывался дважды, когда достаточно одного вызова.