# Návod na filmovou aplikaci v Next.js

> Tento návod vytvoří třístránkový filmový web s Next.js 15 a App Routerem v čistém JavaScriptu. Server komponenty volají amodb přes fetch a revalidate, klíč žije jen v serverové proměnné prostředí a next.config.js povoluje hostitele plakátů img.amodb.dev. Zabere asi 25 minut.

## Co vytvoříte

Vytvoříte malý filmový web se třemi stránkami: seznam trendů na domovské stránce, stránka hledání a stránka filmu se skóre IMDb, Kinopoisk a TMDB. Používá Next.js 15 s App Routerem, který vyžaduje Node.js 18.18 nebo novější, a čistý JavaScript. Server komponenty volají amodb, takže klíč se nikdy nedostane do prohlížeče. Počítejte s asi 25 minutami.

## Krok 1: Získejte API klíč a držte ho na serveru

Zvolte tarif na stránce [ceník](https://amodb.dev/cs/pricing/): 20, 40 nebo 120 požadavků za minutu, se stejnými daty. Vytvořte klíč v panelu účtu a vložte ho do `.env.local` v kořeni projektu:

```bash
AMODB_KEY=YOUR_API_KEY
```

Nepřidávejte k názvu předponu `NEXT_PUBLIC_`. Next.js kopíruje proměnné s touto předponou do balíčku pro prohlížeč, zatímco prostý název zůstává na serveru. Po změně souboru restartujte vývojový server. Podobu klíče v hlavičce a v dotazu popisuje stránka [autentizace](https://amodb.dev/cs/authentication/).

## Krok 2: Vytvořte projekt

Vytvořte `package.json`, nainstalujte balíčky a přidejte hostitele obrázků do `next.config.js`.

```json
{
  "name": "movie-finder",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
```

Nainstalujte balíčky:

```bash
npm install
```

Plakáty jsou na `img.amodb.dev` a `next/image` načítá jen hostitele, které uvedete. Vzor `pathname` ho omezuje na cestu plakátů.

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: 'https', hostname: 'img.amodb.dev', pathname: '/t/p/**' },
    ],
  },
};

module.exports = nextConfig;
```

## Krok 3: Napište pomocnou funkci fetch

Každá stránka prochází jednou funkcí v `lib/amodb.js`. Přidává hlavičku Bearer, 10sekundový timeout a volbu `revalidate`, která Next.js říká, jak dlouho smí odpověď znovu použít. Next.js 15 `fetch` ve výchozím stavu necachuje, takže právě tato volba cache zapíná.

```js
const BASE = 'https://api.amodb.dev/3';

export async function amodb(path, { params = {}, revalidate = 21600 } = {}) {
  const key = process.env.AMODB_KEY;
  if (!key) throw new Error('AMODB_KEY is not set');

  const url = new URL(BASE + path);
  for (const [name, value] of Object.entries(params)) url.searchParams.set(name, value);

  for (let attempt = 0; ; attempt++) {
    const res = await fetch(url.toString(), {
      headers: { Authorization: `Bearer ${key}` },
      next: { revalidate },
      signal: AbortSignal.timeout(10000),
    });

    if (res.status === 429 && attempt < 2) {
      const seconds = Math.min(Number(res.headers.get('retry-after')) || 5, 10);
      await new Promise((resolve) => setTimeout(resolve, seconds * 1000));
      continue;
    }
    if (res.status === 401) throw new Error('amodb rejected the API key (401)');
    if (res.status === 404) return null;
    if (!res.ok) throw new Error(`amodb returned ${res.status}`);
    return res.json();
  }
}
```

Cesta začíná lomítkem a přidává se k základu, takže `/3` zůstává v URL. 404 vrací `null`, takže stránka může zavolat `notFound()`. Opakování čeká nejvýše 10 sekund, protože návštěvník čeká na stránku.

## Krok 4: Sestavte stránky

Začněte rozvržením. Obsahuje formulář hledání, prostý formulář `GET`, který posílá `q` na `/search`.

```js
import Link from 'next/link';

export const metadata = {
  title: 'Movie finder',
  description: 'Trending movies and search, powered by the amodb API.',
};

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body style={{ margin: 0, fontFamily: 'system-ui, sans-serif' }}>
        <header style={{ padding: 16, background: '#111', color: '#fff' }}>
          <Link href="/" style={{ color: '#fff', marginRight: 16 }}>Movie finder</Link>
          <form action="/search" style={{ display: 'inline' }}>
            <input name="q" placeholder="Search movies" />
            <button type="submit">Search</button>
          </form>
        </header>
        <main style={{ maxWidth: 960, margin: '0 auto', padding: 16 }}>{children}</main>
      </body>
    </html>
  );
}
```

Mřížka plakátů v `components/MovieGrid.js` slouží domovské stránce i stránce hledání. URL plakátu je `https://img.amodb.dev/t/p/w342` plus hodnota `poster_path`. Velikosti viz [obrázky](https://amodb.dev/cs/images-api/).

```js
import Image from 'next/image';
import Link from 'next/link';

export default function MovieGrid({ movies }) {
  if (movies.length === 0) return <p>No movies found.</p>;
  return (
    <ul style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(150px, 1fr))', gap: 16, padding: 0, listStyle: 'none' }}>
      {movies.map((movie) => (
        <li key={movie.id}>
          <Link href={`/movie/${movie.id}`}>
            {movie.poster_path && (
              <Image
                src={`https://img.amodb.dev/t/p/w342${movie.poster_path}`}
                alt={movie.title}
                width={342}
                height={513}
                style={{ width: '100%', height: 'auto' }}
              />
            )}
            <strong>{movie.title}</strong>
          </Link>
          <div>{(movie.release_date || '').slice(0, 4)}</div>
        </li>
      ))}
    </ul>
  );
}
```

Domovská stránka v `app/page.js` je asynchronní server komponenta. Čte filmy, které jsou tento týden v trendech, popsané na stránce [trendy a seznamy](https://amodb.dev/cs/trending-lists/).

```js
import MovieGrid from '../components/MovieGrid';
import { amodb } from '../lib/amodb';

export default async function Home() {
  const data = await amodb('/trending/movie/week', {
    params: { language: 'en-US' },
    revalidate: 1800,
  });
  return (
    <section>
      <h1>Trending this week</h1>
      <MovieGrid movies={data.results.slice(0, 18)} />
    </section>
  );
}
```

V Next.js 15 jsou `searchParams` a `params` promisy, takže na ně každá stránka čeká (await). Stránka hledání v `app/search/page.js` používá endpoint [vyhledávání](https://amodb.dev/cs/search/).

```js
import MovieGrid from '../../components/MovieGrid';
import { amodb } from '../../lib/amodb';

export default async function SearchPage({ searchParams }) {
  const { q } = await searchParams;
  const query = (Array.isArray(q) ? q[0] : (q ?? '')).trim();
  if (!query) return <p>Type a title in the box above.</p>;

  const data = await amodb('/search/movie', {
    params: { query, language: 'en-US' },
    revalidate: 600,
  });
  return (
    <section>
      <h1>Results for {query}</h1>
      <MovieGrid movies={data.results} />
    </section>
  );
}
```

Stránka filmu v `app/movie/[id]/page.js` žádá o jeden film s `append_to_response=ratings`, takže detaily a skóre přijdou jedním voláním. Každý zdroj je `{ value, votes }` nebo `null`. Zdroj `null` znamená, že skóre není známé, a `votes` rovné `null` skryje počet hlasů, takže stránka napíše „not known“ nebo vypíše jen hodnotu. Pro film 550 ukazuje `8.8 (2,669,989 votes)` pro IMDb, `8.7` pro Kinopoisk a `8.437 (33,039 votes)` pro TMDB.

```js
import Image from 'next/image';
import { notFound } from 'next/navigation';
import { amodb } from '../../../lib/amodb';

function score(source) {
  if (!source) return 'not known';
  const votes = source.votes ? ` (${source.votes.toLocaleString('en-US')} votes)` : '';
  return `${source.value}${votes}`;
}

export default async function MoviePage({ params }) {
  const { id } = await params;
  if (!/^\d+$/.test(id)) notFound();

  const movie = await amodb(`/movie/${id}`, {
    params: { language: 'en-US', append_to_response: 'ratings' },
  });
  if (!movie) notFound();

  return (
    <article style={{ display: 'flex', gap: 24, flexWrap: 'wrap' }}>
      {movie.poster_path && (
        <Image src={`https://img.amodb.dev/t/p/w500${movie.poster_path}`} alt={movie.title} width={300} height={450} priority />
      )}
      <div style={{ flex: 1, minWidth: 260 }}>
        <h1>{movie.title} ({(movie.release_date || '').slice(0, 4)})</h1>
        {movie.tagline && <p><em>{movie.tagline}</em></p>}
        <p>{movie.overview}</p>
        <ul>
          <li>Runtime: {movie.runtime ? `${movie.runtime} min` : 'not known'}</li>
          <li>IMDb: {score(movie.ratings?.imdb)}</li>
          <li>Kinopoisk: {score(movie.ratings?.kinopoisk)}</li>
          <li>TMDB: {score(movie.ratings?.tmdb)}</li>
        </ul>
      </div>
    </article>
  );
}
```

## Krok 5: Ošetřete chyby a limity požadavků

Pomocná funkce při selhání vyhazuje chybu a Next.js zobrazí nejbližší soubor `error.js`.

| Stav         | Význam                                    | Co dělá aplikace                                                                   |
| ------------ | ----------------------------------------- | ---------------------------------------------------------------------------------- |
| 401          | Klíč chybí, je nesprávný nebo pozastavený | Pomocná funkce vyhodí chybu, vykreslí se `error.js`, příčina je v logu serveru.    |
| 402          | Platnost tarifu skončila                  | Stejná cesta. Obnovte ho na stránce [ceník](https://amodb.dev/cs/pricing/).        |
| 404          | Pro toto ID neexistuje záznam             | Pomocná funkce vrátí `null` a stránka zavolá `notFound()`.                         |
| 429          | Překročili jste požadavky za minutu       | Počká `Retry-After` sekund, nejvýše 10, dvakrát zopakuje pokus a pak vyhodí chybu. |
| 500 nebo 504 | Selhání na straně serveru                 | Pomocná funkce vyhodí chybu a vykreslí se `error.js`.                              |

Hranice chyby musí být klientská komponenta. V produkci Next.js skrývá zprávu o chybě před prohlížečem a vypisuje ji do logu serveru, takže odmítnutý klíč uvidíte v terminálu a ne na stránce. Vytvořte `app/error.js` a `app/not-found.js`:

```js
'use client';

export default function ErrorPage({ reset }) {
  return (
    <div>
      <h2>The movie service did not answer</h2>
      <p>Try again in a moment.</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}
```

Stránka not-found je jediná komponenta:

```js
export default function NotFound() {
  return <p>That movie does not exist.</p>;
}
```

Stránka [chyby](https://amodb.dev/cs/errors/) uvádí všechny stavové kódy a [limity požadavků](https://amodb.dev/cs/rate-limits/) vysvětlují hlavičky.

## Krok 6: Cachujte pomocí revalidate

Hodnoty `revalidate` kopírují doby platnosti cache na edge, takže se stránka nikdy neobnovuje rychleji, než se mění data. Zásah v datové cache Next.js amodb nikdy nevolá, takže nestojí ani žádné požadavky za minutu.

| Stránka       | `revalidate` v sekundách | Doba platnosti cache na edge |
| ------------- | ------------------------ | ---------------------------- |
| Domů, trendy  | 1800                     | 30 minut                     |
| Hledání       | 600                      | 10 minut                     |
| Detaily filmu | 21600                    | 6 hodin                      |

Domovská stránka nemá žádný dynamický vstup, takže ji `next build` vykreslí jednou a na pozadí ji obnovuje každých 30 minut. Tento build volá amodb, takže `AMODB_KEY` musí být nastaven i v prostředí buildu.

## Krok 7: Spusťte a ověřte

```bash
npm run dev
```

Otevřete místní adresu, kterou Next.js vypíše. Domovská stránka ukazuje plakáty, vyhledávací pole najde tituly a plakát otevře stránku svého filmu. Pro produkční běh použijte `npm run build` a `npm run start`. Projekt nyní obsahuje tyto soubory:

| Soubor                             | Role                                                         |
| ---------------------------------- | ------------------------------------------------------------ |
| `.env.local`                       | Klíč, jen na serveru.                                        |
| `next.config.js`                   | Povoluje `img.amodb.dev` pro `next/image`.                   |
| `lib/amodb.js`                     | Pomocná funkce fetch s timeoutem a ošetřením 401, 404 a 429. |
| `app/layout.js`                    | Rámec stránky a formulář hledání.                            |
| `app/page.js`                      | Seznam trendů.                                               |
| `app/search/page.js`               | Výsledky hledání.                                            |
| `app/movie/[id]/page.js`           | Detaily filmu s hodnoceními.                                 |
| `app/error.js`, `app/not-found.js` | Stránky pro selhání a chybějící ID.                          |

## Další kroky

- Přidejte na stránku filmu funkci `generateMetadata`, aby každý titul dostal vlastní název stránky. Next.js znovu použije stejné volání fetch.
- Zobrazte herce pomocí `append_to_response=credits`, jak popisuje stránka [obsazení a štáb](https://amodb.dev/cs/credits/).
- Přepněte `language` na jazyk návštěvníka s využitím 14 základních jazyků na stránce [jazyky](https://amodb.dev/cs/languages/).
- Přečtěte si stejnou úlohu pro telefon v [návodu pro Swift](https://amodb.dev/cs/swift-movie-api/).

## Otázky a odpovědi

### Může klíč API v Next.js uniknout do prohlížeče?

Jen pokud ho zveřejníte. Držte AMODB_KEY bez předpony NEXT_PUBLIC_ a volejte amodb ze server komponent, route handlerů nebo server actions. Nikdy neimportujte pomocnou funkci fetch do souboru, který začíná use client.

### Proč next/image potřebuje remotePatterns pro plakáty?

Komponenta Image optimalizuje jen obrázky z hostitelů, které povolíte. Přidejte img.amodb.dev s pathname /t/p/** do remotePatterns v next.config.js, jinak komponenta vyhodí chybu pro každé URL plakátu.

### Jak dlouhý má být revalidate pro jednotlivé stránky?

Odpovídejte cache na edge: 1800 sekund pro seznamy trendů, 600 pro hledání a 21600 pro detaily filmů. Kratší hodnota jen přidává volání, která vrací stejná data a spotřebovávají požadavky za minutu.

### Co se stane, když amodb odpoví 429?

Pomocná funkce přečte Retry-After, počká až 10 sekund a zopakuje pokus dvakrát. Pokud limit stále platí, vyhodí chybu a error.js vykreslí stránku s tlačítkem pro opakování místo rozbitého seznamu.

### Potřebuji pro tuto aplikaci TypeScript?

Ne. Každý soubor je zde čistý JavaScript s JSX v souborech .js. Funkce App Routeru fungují stejně a soubory můžete později převést na TypeScript, aniž byste měnili jakékoli volání amodb.

## Související stránky

- [Návod na filmové API ve Swiftu se SwiftUI](https://amodb.dev/cs/swift-movie-api/): Vytvořte aplikaci SwiftUI pro iOS 16 se seznamem trendových filmů s plakáty přes URLSession async/await a AsyncImage a zjistěte, proč patří klíč na server.
- [Návod na filmové API v JavaScriptu](https://amodb.dev/cs/javascript-movie-api/): Napište skript v Node.js, který jedním požadavkem na ID načte detaily, obsazení a hodnocení filmů, s cache s TTL, opakováním při 429 a bez závislostí.
- [API vyhledávání](https://amodb.dev/cs/search/): API vyhledávání se stejnými cestami a JSON jako TMDB v3. Najděte filmy, seriály, osoby, kolekce, společnosti a klíčová slova podle textu s filtrem roku.
- [Trendy a seznamy](https://amodb.dev/cs/trending-lists/): API trendů filmů s cestami TMDB v3: denní a týdenní žebříčky a seznamy now playing, popular, top rated a upcoming pro filmy a seriály, cachované 30 minut.
- [API obrázků a hostitel obrázků](https://amodb.dev/cs/images-api/): API obrázků filmů s cestami TMDB v3: vypište plakáty, pozadí, loga a záběry každého titulu a načtěte je z img.amodb.dev v 10 velikostech bez klíče.
- [Autentizace](https://amodb.dev/cs/authentication/): Jak poslat klíč amodb: api_key v dotazu, hlavička Authorization Bearer nebo apikey pro volání OMDb. Formát klíče, uložení, rotace a chyby 401.

Aktualizováno: 2026-10-18. Source: https://amodb.dev/cs/nextjs-movie-app/
