Leçon 6 / 20

Filtrer le catalogue par difficulté

25 min Next.js — données, formulaires et cache Gratuit

Objectif : Retourner depuis le serveur les randonnées correspondant à un niveau de difficulté choisi.

Un catalogue peut contenir des randonnées de niveaux très différents. Sans filtre, une personne doit parcourir toute la liste pour trouver une sortie adaptée. Vous allez faire porter le choix de difficulté par l’URL, puis transmettre au serveur une valeur contrôlée afin que la requête retourne uniquement les résultats correspondants.

La page catalogue reste un composant serveur et continue d’utiliser la fonction d’accès aux données `obtenirRandonnees`, mise en place dans la leçon précédente. Ici, vous ajoutez un paramètre à cette fonction et reliez la valeur sélectionnée dans le formulaire à ce paramètre. Le navigateur ne reçoit pas de clé ni d’accès à la base : il demande simplement une URL telle que `/?difficulte=facile`.

Lire et valider le filtre

Dans l’App Router, les paramètres de recherche sont fournis à la page dans `searchParams`. Avec les versions récentes de Next.js, cette propriété est asynchrone : attendez-la avant d’en lire les valeurs. Un paramètre provenant de l’URL est une entrée utilisateur ; il peut être absent, répété ou contenir une valeur inattendue. Ne l’envoyez donc pas directement à l’API.

Compléter la fonction d’accès aux données

La fonction serveur doit maintenant accepter un objet contenant éventuellement `difficulte`. Construisez l’URL de l’API avec `URL` et `URLSearchParams` plutôt que de concaténer une chaîne : les caractères sont encodés correctement et le paramètre n’est ajouté que lorsqu’un filtre valide est présent. Conservez la vérification du statut HTTP déjà réalisée dans cette couche ; une réponse en erreur doit toujours être signalée comme une erreur, pas transformée en liste vide.

app/page.tsx — couche serveur et page catalogueTexte
import 'server-only';

const difficultes = ['facile', 'intermediaire', 'difficile'] as const;
type Difficulte = (typeof difficultes)[number];

type Randonnee = {
  id: string;
  nom: string;
  difficulte: Difficulte;
  distance: number;
};

function validerDifficulte(valeur: string | string[] | undefined): Difficulte | undefined {
  if (typeof valeur !== 'string') return undefined;
  return difficultes.includes(valeur as Difficulte) ? valeur as Difficulte : undefined;
}

async function obtenirRandonnees(options: { difficulte?: Difficulte }): Promise<Randonnee[]> {
  const apiUrl = process.env.CATALOGUE_API_URL;
  if (!apiUrl) throw new Error('La variable CATALOGUE_API_URL est absente.');

  const url = new URL('/randonnees', apiUrl);
  if (options.difficulte) {
    url.searchParams.set('difficulte', options.difficulte);
  }

  const reponse = await fetch(url);
  if (!reponse.ok) {
    throw new Error(`Échec du chargement du catalogue : ${reponse.status}`);
  }

  return await reponse.json() as Randonnee[];
}

export default async function CataloguePage({
  searchParams
}: {
  searchParams: Promise<{ difficulte?: string | string[] }>;
}) {
  const parametres = await searchParams;
  const difficulte = validerDifficulte(parametres.difficulte);
  const randonnees = await obtenirRandonnees({ difficulte });

  return (
    <>
      <h1 className="rando-titre">Randonnées à découvrir</h1>
      <form className="rando-recherche" action="/">
        <label htmlFor="difficulte">Difficulté</label>
        <select id="difficulte" name="difficulte" defaultValue={difficulte ?? ''}>
          <option value="">Toutes</option>
          <option value="facile">Facile</option>
          <option value="intermediaire">Intermédiaire</option>
          <option value="difficile">Difficile</option>
        </select>
        <button className="rando-bouton" type="submit">Rechercher</button>
      </form>
      {randonnees.length === 0 ? (
        <p className="rando-etat" role="status">Aucune randonnée ne correspond à cette recherche.</p>
      ) : (
        <ul className="rando-liste">
          {randonnees.map((randonnee) => (
            <li className="rando-carte" key={randonnee.id}>
              <a href={`/randonnees/${randonnee.id}`}>
                <h2>{randonnee.nom}</h2>
                <p>{randonnee.difficulte} · {randonnee.distance} km</p>
              </a>
            </li>
          ))}
        </ul>
      )}
    </>
  );
}

Ce fichier constitue l’état complet de `app/page.tsx` après la modification. Si votre couche d’accès est conservée dans un module serveur séparé, gardez-y le même contrat : `obtenirRandonnees({ difficulte })` construit la requête filtrée et vérifie la réponse. Ici, la validation transforme le paramètre reçu en l’un des trois choix autorisés ou en absence de filtre.

Vérifier le parcours

Quand vous choisissez « Facile » puis soumettez le formulaire, le navigateur visite `/?difficulte=facile`. La page serveur valide le paramètre et l’API reçoit une requête de la forme `https://api.exemple.test/randonnees?difficulte=facile` ; la liste affichée contient alors les randonnées correspondant à ce niveau. Avec « Toutes », aucun paramètre de difficulté n’est ajouté et l’API peut renvoyer le catalogue complet.

Testez aussi une URL modifiée manuellement, par exemple `/?difficulte=inconnue`. Le filtre invalide n’est pas transmis à l’API et la page revient au choix neutre « Toutes ». Les réponses vides et les exceptions de requête restent deux situations distinctes ; la gestion explicite de la liste sans résultat sera traitée dans une leçon dédiée.

À retenir

  • Le formulaire GET place la difficulté choisie dans l’URL ; la page serveur lit cette valeur dans `searchParams`.
  • Traitez le paramètre comme une entrée non fiable et acceptez uniquement `facile`, `intermediaire` ou `difficile`.
  • Transmettez à la couche d’accès aux données un filtre validé, ou `undefined` pour demander le catalogue sans filtre.
  • Construisez l’URL de l’API avec `URL` et `searchParams`, puis conservez la vérification du statut HTTP.
  • Une valeur invalide ne doit pas élargir la requête avec une valeur arbitraire : elle revient au filtre neutre.

Dans la prochaine leçon, vous choisirez comment mettre en cache les réponses de lecture et définir leur durée de revalidation.

Envie d'aller plus loin ?

Les cours Enovatic sont gratuits et le resteront. Si vous cherchez un accompagnement encadré, nos formations professionnelles sont animées par les mêmes formateurs.