# Tutorial app film con Next.js

> Questo tutorial costruisce un sito di film di tre pagine con Next.js 15 e l'App Router in JavaScript. I server component chiamano amodb con fetch e revalidate, la chiave vive solo in una variabile d'ambiente del server, e next.config.js consente l'host delle locandine img.amodb.dev. Richiede circa 25 minuti.

## Cosa costruisci

Costruisci un piccolo sito di film con tre pagine: una lista di tendenze nella home, una pagina di ricerca e una pagina del film con i punteggi IMDb, Kinopoisk e TMDB. Usa Next.js 15 con l'App Router, che richiede Node.js 18.18 o successivo, e JavaScript semplice. I server component chiamano amodb, quindi la chiave non arriva mai al browser. Calcola circa 25 minuti.

## Passaggio 1: ottieni una chiave API e tienila sul server

Scegli un piano nella pagina dei [prezzi](https://amodb.dev/it/pricing/): 20, 40 o 120 richieste al minuto, con dati identici. Crea una chiave nella dashboard del tuo account, poi mettila in `.env.local` nella radice del progetto:

```bash
AMODB_KEY=YOUR_API_KEY
```

Non aggiungere il prefisso `NEXT_PUBLIC_` al nome. Next.js copia nel bundle del browser le variabili con quel prefisso, mentre un nome semplice resta sul server. Riavvia il server di sviluppo dopo aver modificato il file. La pagina [autenticazione](https://amodb.dev/it/authentication/) descrive le forme della chiave come header e come parametro di query.

## Passaggio 2: crea il progetto

Crea `package.json`, installa i pacchetti e aggiungi l'host delle immagini a `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"
  }
}
```

Installa i pacchetti:

```bash
npm install
```

Le locandine stanno su `img.amodb.dev`, e `next/image` carica solo gli host che elenchi. Il pattern `pathname` lo limita al percorso delle locandine.

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

module.exports = nextConfig;
```

## Passaggio 3: scrivi l'helper fetch

Ogni pagina passa da una sola funzione in `lib/amodb.js`. Aggiunge l'header Bearer, un timeout di 10 secondi e l'opzione `revalidate`, che dice a Next.js per quanto tempo può riusare la risposta. Next.js 15 non mette `fetch` in cache per impostazione predefinita, quindi è questa opzione ad attivare la cache.

```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();
  }
}
```

Il percorso inizia con una barra e viene aggiunto alla base, quindi `/3` resta nell'URL. Un 404 restituisce `null`, così una pagina può chiamare `notFound()`. Il nuovo tentativo attende al massimo 10 secondi perché un visitatore sta aspettando la pagina.

## Passaggio 4: costruisci le pagine

Inizia dal layout. Contiene il modulo di ricerca, un semplice form `GET` che invia `q` a `/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>
  );
}
```

La griglia di locandine in `components/MovieGrid.js` serve la home e la pagina di ricerca. L'URL di una locandina è `https://img.amodb.dev/t/p/w342` più il valore di `poster_path`. Vedi [immagini](https://amodb.dev/it/images-api/) per le dimensioni.

```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>
  );
}
```

La home in `app/page.js` è un server component asincrono. Legge i film di tendenza della settimana, descritti nella pagina [tendenze e liste](https://amodb.dev/it/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>
  );
}
```

In Next.js 15, `searchParams` e `params` sono promise, quindi ogni pagina le attende con await. La pagina di ricerca in `app/search/page.js` usa l'endpoint di [ricerca](https://amodb.dev/it/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>
  );
}
```

La pagina del film in `app/movie/[id]/page.js` richiede un film con `append_to_response=ratings`, così dettagli e punteggi arrivano in un'unica chiamata. Ogni fonte è `{ value, votes }` oppure `null`. Una fonte `null` significa che il punteggio non è noto, e un `votes` a `null` nasconde il numero di voti, quindi la pagina scrive «non noto» o stampa solo il valore. Per il film 550 mostra `8.8 (2,669,989 votes)` per IMDb, `8.7` per Kinopoisk e `8.437 (33,039 votes)` per 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>
  );
}
```

## Passaggio 5: gestisci errori e limiti di richieste

L'helper solleva un'eccezione in caso di errore, e Next.js mostra il file `error.js` più vicino.

| Stato     | Significato                          | Cosa fa l'app                                                                                          |
| --------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------ |
| 401       | Chiave mancante, sbagliata o sospesa | L'helper solleva un'eccezione, `error.js` si mostra, la causa è nel log del server.                    |
| 402       | Il periodo del piano è terminato     | Stesso percorso. Rinnova nella pagina dei [prezzi](https://amodb.dev/it/pricing/).                     |
| 404       | Nessun record per quell'ID           | L'helper restituisce `null` e la pagina chiama `notFound()`.                                           |
| 429       | Oltre le tue richieste al minuto     | Attende i secondi di `Retry-After`, con un massimo di 10, ritenta due volte, poi solleva un'eccezione. |
| 500 o 504 | Errore lato server                   | L'helper solleva un'eccezione e `error.js` si mostra.                                                  |

Un error boundary deve essere un client component. In produzione Next.js nasconde il messaggio di errore al browser e lo scrive nel log del server, quindi una chiave rifiutata compare nel tuo terminale e non nella pagina. Crea `app/error.js` e `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>
  );
}
```

La pagina non trovata è un solo componente:

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

La pagina [errori](https://amodb.dev/it/errors/) elenca tutti i codici di stato, e [limiti di richieste](https://amodb.dev/it/rate-limits/) spiega gli header.

## Passaggio 6: usa la cache con revalidate

I valori di `revalidate` copiano i tempi di vita della cache edge, quindi una pagina non si aggiorna mai più in fretta di quanto cambino i dati. Un hit nella data cache di Next.js non chiama mai amodb, quindi non costa nemmeno richieste al minuto.

| Pagina            | `revalidate` in secondi | Durata della cache edge |
| ----------------- | ----------------------- | ----------------------- |
| Home, tendenze    | 1800                    | 30 minuti               |
| Ricerca           | 600                     | 10 minuti               |
| Dettagli del film | 21600                   | 6 ore                   |

La home non ha input dinamici, quindi `next build` la genera una volta e la aggiorna in background ogni 30 minuti. Quella build chiama amodb, quindi `AMODB_KEY` deve essere impostata anche nell'ambiente di build.

## Passaggio 7: avvia e verifica

```bash
npm run dev
```

Apri l'indirizzo locale che stampa Next.js. La home mostra le locandine, la casella di ricerca trova i titoli e una locandina apre la sua pagina del film. Per un'esecuzione in produzione usa `npm run build` e `npm run start`. Il progetto ora ha questi file:

| File                               | Ruolo                                                  |
| ---------------------------------- | ------------------------------------------------------ |
| `.env.local`                       | La chiave, solo sul server.                            |
| `next.config.js`                   | Consente `img.amodb.dev` per `next/image`.             |
| `lib/amodb.js`                     | Helper fetch con timeout e gestione di 401, 404 e 429. |
| `app/layout.js`                    | Struttura della pagina e modulo di ricerca.            |
| `app/page.js`                      | Lista delle tendenze.                                  |
| `app/search/page.js`               | Risultati di ricerca.                                  |
| `app/movie/[id]/page.js`           | Dettagli del film con le valutazioni.                  |
| `app/error.js`, `app/not-found.js` | Pagine di errore e di ID mancante.                     |

## Prossimi passi

- Aggiungi una funzione `generateMetadata` alla pagina del film così ogni titolo ha il proprio titolo di pagina. Next.js riusa la stessa chiamata fetch.
- Mostra il cast con `append_to_response=credits`, descritto nella pagina [crediti](https://amodb.dev/it/credits/).
- Passa `language` alla lingua del visitatore, usando le 14 lingue di base elencate nella pagina [lingue](https://amodb.dev/it/languages/).
- Leggi lo stesso compito per un telefono nel [tutorial Swift](https://amodb.dev/it/swift-movie-api/).

## Domande e risposte

### La chiave API può trapelare nel browser in Next.js?

Solo se la esponi. Tieni AMODB_KEY senza il prefisso NEXT_PUBLIC_ e chiama amodb da server component, route handler o server action. Non importare mai l'helper fetch in un file che inizia con use client.

### Perché next/image ha bisogno di remotePatterns per le locandine?

Il componente Image ottimizza solo le immagini degli host che consenti. Aggiungi img.amodb.dev con il pathname /t/p/** a remotePatterns in next.config.js, altrimenti il componente genera un errore per ogni URL di locandina.

### Quanto deve durare revalidate per ogni pagina?

Allineati alla cache edge: 1800 secondi per le liste di tendenze, 600 per la ricerca e 21600 per i dettagli dei film. Un valore più breve aggiunge solo chiamate che restituiscono gli stessi dati e consumano richieste al minuto.

### Cosa succede quando amodb risponde 429?

L'helper legge Retry-After, attende fino a 10 secondi e ritenta due volte. Se il limite vale ancora, solleva un errore, e error.js mostra una pagina con un pulsante per riprovare invece di una lista rotta.

### Mi serve TypeScript per questa app?

No. Ogni file qui è JavaScript semplice con JSX in file .js. Le funzioni dell'App Router funzionano allo stesso modo, e potrai convertire i file in TypeScript più avanti senza cambiare nessuna chiamata ad amodb.

## Pagine correlate

- [Tutorial API film Swift con SwiftUI](https://amodb.dev/it/swift-movie-api/): Un'app SwiftUI per iOS 16 che elenca i film di tendenza con locandine tramite URLSession async/await e AsyncImage, e perché la chiave deve stare su un server.
- [Tutorial API film JavaScript](https://amodb.dev/it/javascript-movie-api/): Uno script Node.js che carica dettagli, crediti e valutazioni di un elenco di ID film con una richiesta ciascuno, cache TTL, retry su 429 e zero dipendenze.
- [API di ricerca](https://amodb.dev/it/search/): API di ricerca con percorsi e JSON di TMDB v3. Trova film, serie, persone, collezioni, società e parole chiave per testo, con filtri anno e paginazione.
- [Tendenze e endpoint delle liste](https://amodb.dev/it/trending-lists/): API tendenze con percorsi TMDB v3: classifiche giorno e settimana, più liste di film e serie in sala, popolari, più votati e in arrivo, cache di 30 minuti.
- [API delle immagini e host delle immagini](https://amodb.dev/it/images-api/): API immagini dei film con percorsi TMDB v3: elenca locandine, sfondi, loghi e fermi immagine di ogni titolo e caricali da img.amodb.dev in 10 dimensioni.
- [Autenticazione](https://amodb.dev/it/authentication/): Come inviare la chiave amodb: api_key nella query, un header Authorization Bearer o apikey per le chiamate OMDb. Formato, conservazione, rotazione, errori 401.

Aggiornato: 2026-10-18. Source: https://amodb.dev/it/nextjs-movie-app/
