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 : 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 :
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 décrit les formes en en-tête et en paramètre de requête de la clé.
AMODB_KEY=YOUR_API_KEYÉtape 2 : créer le projet
Créez package.json, installez les paquets et ajoutez l'hôte d'images à next.config.js.
{
"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 :
npm installLes 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.
/** @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.
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.
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();
}
}É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.
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/ sert la page d'accueil et la page de recherche. L'URL d'une affiche est https:/ plus la valeur de poster_path. Voir images pour les tailles.
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.
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.
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_, 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.
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. |
| 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 :
'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 :
La page erreurs liste tous les codes de statut, et limites de débit explique les en-têtes.
export default function NotFound() {
return <p>That movie does not exist.</p>;
}É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
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. |
npm run devPour 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_, décrit sur crédits.to_ response= credits - Faites passer
languageà la langue du visiteur, avec les 14 langues de base listées sur langues. - Lisez la même tâche pour un téléphone dans le tutoriel Swift.