# Tutoriel application de films Next.js

> Ce tutoriel construit un site de films de trois pages avec Next.js 15 et l'App Router en JavaScript. Les composants serveur appellent amodb avec fetch et revalidate, la clé ne vit que dans une variable d'environnement serveur, et next.config.js autorise l'hôte d'affiches img.amodb.dev. Il prend environ 25 minutes.

## Ce que vous construisez

Vous construisez un petit site de films avec trois pages : une liste de tendances sur la page d'accueil, une page de recherche et une page de film avec les notes IMDb, Kinopoisk et TMDB. Il utilise Next.js 15 avec l'App Router, qui demande Node.js 18.18 ou plus récent, et du JavaScript simple. Les composants serveur appellent amodb : la clé n'atteint donc jamais le navigateur. Prévoyez environ 25 minutes.

## Étape 1 : obtenir une clé d'API et la garder côté serveur

Choisissez une offre sur les [tarifs](https://amodb.dev/fr/pricing/) : 20, 40 ou 120 requêtes par minute, avec des données identiques. Créez une clé dans le tableau de bord de votre compte, puis placez-la dans `.env.local` à la racine du projet :

```bash
AMODB_KEY=YOUR_API_KEY
```

Ne préfixez pas le nom avec `NEXT_PUBLIC_`. Next.js copie les variables qui ont ce préfixe dans le bundle du navigateur, alors qu'un nom simple reste côté serveur. Redémarrez le serveur de développement après avoir modifié le fichier. La page [authentification](https://amodb.dev/fr/authentication/) décrit les formes en en-tête et en paramètre de requête de la clé.

## Étape 2 : créer le projet

Créez `package.json`, installez les paquets et ajoutez l'hôte d'images à `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"
  }
}
```

Installez les paquets :

```bash
npm install
```

Les affiches se trouvent sur `img.amodb.dev`, et `next/image` ne charge que les hôtes que vous listez. Le motif `pathname` le limite au chemin des affiches.

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

module.exports = nextConfig;
```

## Étape 3 : écrire l'assistant fetch

Chaque page passe par une seule fonction dans `lib/amodb.js`. Elle ajoute l'en-tête Bearer, un délai d'expiration de 10 secondes et l'option `revalidate`, qui indique à Next.js combien de temps il peut réutiliser la réponse. Next.js 15 ne met pas `fetch` en cache par défaut : c'est donc cette option qui active le 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();
  }
}
```

Le chemin commence par une barre oblique et s'ajoute à la base, si bien que `/3` reste dans l'URL. Un 404 renvoie `null`, ce qui permet à une page d'appeler `notFound()`. La nouvelle tentative attend au plus 10 secondes, car un visiteur attend la page.

## Étape 4 : construire les pages

Commencez par la mise en page. Elle contient le formulaire de recherche, un simple formulaire `GET` qui envoie `q` à `/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 grille d'affiches dans `components/MovieGrid.js` sert la page d'accueil et la page de recherche. L'URL d'une affiche est `https://img.amodb.dev/t/p/w342` plus la valeur de `poster_path`. Voir [images](https://amodb.dev/fr/images-api/) pour les tailles.

```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 page d'accueil dans `app/page.js` est un composant serveur asynchrone. Elle lit les films tendance de la semaine, décrits sur [tendances et listes](https://amodb.dev/fr/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>
  );
}
```

Dans Next.js 15, `searchParams` et `params` sont des promesses : chaque page les attend donc avec await. La page de recherche dans `app/search/page.js` utilise le point de terminaison de [recherche](https://amodb.dev/fr/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 page de film dans `app/movie/[id]/page.js` demande un film avec `append_to_response=ratings`, si bien que les détails et les notes arrivent en un seul appel. Chaque source est `{ value, votes }` ou `null`. Une source `null` signifie que la note n'est pas connue, et un `votes` à `null` masque le nombre de votes : la page affiche donc « inconnue » ou seulement la valeur. Pour le film 550, elle affiche `8.8 (2,669,989 votes)` pour IMDb, `8.7` pour Kinopoisk et `8.437 (33,039 votes)` pour 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>
  );
}
```

## Étape 5 : gérer les erreurs et les limites de débit

L'assistant lève une exception en cas d'échec, et Next.js affiche le fichier `error.js` le plus proche.

| Statut     | Signification                        | Ce que fait l'application                                                                        |
| ---------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ |
| 401        | Clé absente, fausse ou suspendue     | L'assistant lève une exception, `error.js` s'affiche, la cause est dans le journal du serveur.   |
| 402        | La période de l'offre est terminée   | Même chemin. Renouveler sur les [tarifs](https://amodb.dev/fr/pricing/).                         |
| 404        | Aucune fiche pour cet identifiant    | L'assistant renvoie `null` et la page appelle `notFound()`.                                      |
| 429        | Au-dessus de vos requêtes par minute | Attendre `Retry-After` secondes, plafonnées à 10, réessayer deux fois, puis lever une exception. |
| 500 ou 504 | Échec côté serveur                   | L'assistant lève une exception et `error.js` s'affiche.                                          |

Une limite d'erreur doit être un composant client. En production, Next.js masque le message d'erreur au navigateur et l'écrit dans le journal du serveur : une clé rejetée apparaît donc dans votre terminal et non sur la page. Créez `app/error.js` et `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 page introuvable est un seul composant :

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

La page [erreurs](https://amodb.dev/fr/errors/) liste tous les codes de statut, et [limites de débit](https://amodb.dev/fr/rate-limits/) explique les en-têtes.

## Étape 6 : mettre en cache avec revalidate

Les valeurs de `revalidate` reprennent les durées de vie du cache de périphérie : une page ne s'actualise donc jamais plus vite que les données ne changent. Un succès dans le cache de données de Next.js n'appelle jamais amodb, il ne coûte donc aucune requête par minute non plus.

| Page               | `revalidate` en secondes | Durée de vie du cache de périphérie |
| ------------------ | ------------------------ | ----------------------------------- |
| Accueil, tendances | 1800                     | 30 minutes                          |
| Recherche          | 600                      | 10 minutes                          |
| Détails d'un film  | 21600                    | 6 heures                            |

La page d'accueil n'a aucune entrée dynamique : `next build` la génère donc une fois et l'actualise en arrière-plan toutes les 30 minutes. Cette génération appelle amodb, donc `AMODB_KEY` doit aussi être définie dans l'environnement de build.

## Étape 7 : lancer et vérifier

```bash
npm run dev
```

Ouvrez l'adresse locale qu'affiche Next.js. La page d'accueil montre des affiches, la zone de recherche trouve des titres et une affiche ouvre sa page de film. Pour une exécution en production, utilisez `npm run build` et `npm run start`. Le projet contient maintenant ces fichiers :

| Fichier                            | Rôle                                                                    |
| ---------------------------------- | ----------------------------------------------------------------------- |
| `.env.local`                       | La clé, côté serveur uniquement.                                        |
| `next.config.js`                   | Autorise `img.amodb.dev` pour `next/image`.                             |
| `lib/amodb.js`                     | Assistant fetch avec délai d'expiration et gestion des 401, 404 et 429. |
| `app/layout.js`                    | Structure de page et formulaire de recherche.                           |
| `app/page.js`                      | Liste des tendances.                                                    |
| `app/search/page.js`               | Résultats de recherche.                                                 |
| `app/movie/[id]/page.js`           | Détails d'un film avec les notes.                                       |
| `app/error.js`, `app/not-found.js` | Pages d'échec et d'identifiant introuvable.                             |

## Pour aller plus loin

- Ajoutez une fonction `generateMetadata` à la page de film pour que chaque titre ait son propre titre de page. Next.js réutilise le même appel fetch.
- Affichez la distribution avec `append_to_response=credits`, décrit sur [crédits](https://amodb.dev/fr/credits/).
- Faites passer `language` à la langue du visiteur, avec les 14 langues de base listées sur [langues](https://amodb.dev/fr/languages/).
- Lisez la même tâche pour un téléphone dans le [tutoriel Swift](https://amodb.dev/fr/swift-movie-api/).

## Questions et réponses

### La clé d'API peut-elle fuiter vers le navigateur dans Next.js ?

Seulement si vous l'exposez. Gardez AMODB_KEY sans le préfixe NEXT_PUBLIC_ et appelez amodb depuis des composants serveur, des route handlers ou des server actions. N'importez jamais l'assistant fetch dans un fichier qui commence par use client.

### Pourquoi next/image a-t-il besoin de remotePatterns pour les affiches ?

Le composant Image n'optimise que les images des hôtes que vous autorisez. Ajoutez img.amodb.dev avec le pathname /t/p/** à remotePatterns dans next.config.js, sinon le composant lève une erreur pour chaque URL d'affiche.

### Quelle durée de revalidate pour chaque page ?

Calquez-vous sur le cache de périphérie : 1800 secondes pour les listes de tendances, 600 pour la recherche et 21600 pour les détails de films. Une valeur plus courte n'ajoute que des appels qui renvoient les mêmes données et consomment des requêtes par minute.

### Que se passe-t-il quand amodb répond 429 ?

L'assistant lit Retry-After, attend jusqu'à 10 secondes et réessaie deux fois. Si la limite s'applique encore, il lève une erreur, et error.js affiche une page avec un bouton de nouvelle tentative au lieu d'une liste cassée.

### Ai-je besoin de TypeScript pour cette application ?

Non. Tous les fichiers ici sont du JavaScript simple avec du JSX dans des fichiers .js. Les fonctions de l'App Router marchent de la même façon, et vous pourrez convertir les fichiers en TypeScript plus tard sans changer aucun appel à amodb.

## Pages liées

- [Tutoriel API de films Swift avec SwiftUI](https://amodb.dev/fr/swift-movie-api/): Une app SwiftUI pour iOS 16 qui liste les films tendance avec affiches via URLSession async/await et AsyncImage, et pourquoi la clé doit rester sur un serveur.
- [Tutoriel API de films JavaScript](https://amodb.dev/fr/javascript-movie-api/): Un script Node.js qui charge détails, crédits et notes d'une liste d'ids de films en une requête chacun, avec cache TTL, relance 429 et sans dépendance.
- [API de recherche](https://amodb.dev/fr/search/): API de recherche aux chemins et au JSON de TMDB v3 : films, séries, personnes, collections, sociétés et mots-clés par texte, filtres d'année et pagination.
- [Tendances et points de terminaison de listes](https://amodb.dev/fr/trending-lists/): API de tendances aux chemins TMDB v3 : classements jour et semaine, plus films et séries à l'affiche, populaires, mieux notés et à venir, cache de 30 minutes.
- [API d'images et hôte d'images](https://amodb.dev/fr/images-api/): API d'images de films aux chemins TMDB v3 : listez affiches, fonds, logos et images par titre, puis chargez-les depuis img.amodb.dev en 10 tailles sans clé.
- [Authentification](https://amodb.dev/fr/authentication/): Comment envoyer votre clé amodb : api_key dans la requête, en-tête Authorization Bearer, ou apikey pour OMDb. Format, stockage, rotation et erreurs 401.

Mis à jour: 2026-10-18. Source: https://amodb.dev/fr/nextjs-movie-app/
