Головна / Статті

Створення стійкої сторінки деталей фільму за допомогою 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, який необхідно очекувати перед використанням. Якщо помістити цей старий код у новостворений проект за допомогою команди 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; тому потрібна перевірка на кшталт movie.Poster !== "N/A" перед передачею значення до next/image — якщо передати "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 — кеш, який заповнюється лише у разі неможливості отримати дані безпосередньо від OMDB. Усе, що описано вище, по суті становить першу половину такої архітектури.

Головний висновок полягає у тому, що «масштабованість» у цьому контексті — це не якась додаткова платформа, яку можна додати пізніше. Все зводиться до правильного встановлення значення параметра revalidate, повернення належного коду 404 та переконанняся, що ви ніколи не викликаєте API двічі, коли достатньо одного виклику.