Главная / Статьи

Создание устойчивой страницы с деталями фильма с использованием Next.js App Router

Загружайте и храните в кэше данные OMDB в Next.js App Router с использованием асинхронных серверных компонентов, оператора awaited для параметров и реальной обработки ошибки 404 для страницы фильма, чтобы она не выходила из строя.

Издатель: REACTAPP.TOP7 мин чтения1556 слов

Источники: 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 не вызывался дважды, когда достаточно одного вызова.