CodeGym /Cours /Spring REST & MVC /@RequestParam pour les filtres et les valeurs par défaut

@RequestParam pour les filtres et les valeurs par défaut

Spring REST & MVC
Niveau 6, Leçon 1
Disponible

1. Paramètres de requête pour l'endpoint de liste

Nous avons déjà abordé l'adressage : une ressource spécifique se trouve dans le path, et une liste nécessite presque immédiatement des conditions supplémentaires. GET /api/v1/tasks sans filtres est un bon début, mais dès que vous avez besoin de status, assigneeName, page ou size, créer un nouvel URI pour chaque variation devient pénible.

Pour de telles précisions, HTTP dispose de la chaîne de requête. Elle ne modifie pas l'adresse de la collection, mais paramètre la sélection : quels éléments afficher, dans quel volume et avec quelles limitations. C'est pourquoi GET /api/v1/tasks?status=TODO&page=0 reste une requête vers la même collection de tâches, et non une nouvelle ressource.

Aujourd'hui, nous nous concentrons délibérément sur le bloc Query + @RequestParam : comment rendre le contrat de la liste lisible, prévisible, et ne pas transformer le contrôleur en une méthode qui s'étale sur un demi-écran.

2. Comment fonctionne @RequestParam

En HTTP, les paramètres de requête se trouvent après ? et sont séparés par &. Par exemple :

/api/v1/tasks?status=TODO

/api/v1/tasks?page=0&size=20

/api/v1/tasks?status=IN_PROGRESS&assigneeName=Anna&page=1

Du point de vue du navigateur ou du client, ce n'est qu'une chaîne. Du point de vue de Spring MVC, ce sont des données d'entrée qu'il faut extraire de la requête, convertir proprement dans le type attendu et transmettre à la méthode du contrôleur. C'est exactement ce lien « valeur dans la chaîne de requête → argument de méthode » qu'exprime @RequestParam.

L'exemple minimal le plus « honnête » ressemble à ceci : nous voulons pouvoir accepter un filtre par statut.

import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public void findTasks(@RequestParam TaskStatus status) {
    // Important : le paramètre est obligatoire par défaut (required = true).
    // Spring prendra la valeur de la chaîne de requête (?status=...) et la convertira en TaskStatus.
    // Si le paramètre n'est pas passé, la requête ne passera pas la validation de liaison au niveau de Spring MVC.
}

Ici, Spring s'attend à ce que le client appelle l'endpoint de cette manière :

GET http://localhost:8080/api/v1/tasks?status=TODO
Accept: application/json

Un point important pour l'état d'esprit du développeur back-end : le simple fait d'avoir @RequestParam fait partie du contrat public de l'API. Le client n'a pas besoin de lire votre code service ou votre repository pour comprendre comment « paramétrer » la requête : il le voit dans l'URL et (plus tard) dans la documentation.

On voit déjà un autre aspect important : dans l'URL se trouve la chaîne status=TODO, et dans la méthode, TaskStatus. Il y a donc une couche de conversion entre la valeur brute de la requête et l'argument du contrôleur, à cause de laquelle une valeur incorrecte peut échouer avant même la logique métier. Nous analyserons plus tard les mécanismes de ce binding sur les enums, les nombres et les dates.

Dans notre projet Task Tracker API, l'endpoint de liste est GET /api/v1/tasks, et c'est via les paramètres de requête que nous allons progressivement enrichir ses fonctionnalités (aujourd'hui, seulement le squelette d'entrée, pas « toute la pagination du monde »).

3. Paramètre de requête obligatoire et optionnel

Si vous commencez à ajouter @RequestParam sans réfléchir dans la méthode, vous risquez de rendre votre endpoint « fragile » : il commencera à exiger des paramètres même lorsque cela n'a pas de sens. Pour un endpoint de liste, c'est particulièrement dangereux, car « la liste de toutes les tâches » est un scénario normal. Et il doit fonctionner sans que le client soit obligé de fournir status ou assigneeName.

Par défaut, @RequestParam est considéré comme obligatoire. C'est-à-dire que si vous écrivez @RequestParam String q, Spring attendra ?q=.... Si ce n'est pas fourni, la requête échouera probablement (nous n'entrerons pas dans les détails des erreurs aujourd'hui, mais le principe est important).

Comment rendre un paramètre optionnel : required = false

Pour dire à Spring : « Ce paramètre peut être présent ou non », vous définissez required = false. Alors, en l'absence du paramètre, Spring transmettra null (ou une enveloppe vide — selon le type).

import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public void findTasks(@RequestParam(required = false) TaskStatus status) {
    // Si ?status=... n'est pas passé, Spring mettra null.
    // C'est pratique pour les filtres : null est généralement interprété comme « filtre désactivé ».
}

C'est un bon modèle de base pour les filtres. Un filtre « par statut » est comme une case à cocher dans l'interface utilisateur : l'utilisateur peut l'activer ou non.

Pourquoi le « paramètre optionnel » s'entend mal avec les primitifs

C'est un piège typique pour les débutants, qui semble amusant jusqu'au moment où vous passez une demi-heure à déboguer.

Si un paramètre peut être absent, il peut être null. Et les primitifs (int, boolean) ne savent pas gérer null. En conséquence, un code du genre « eh bien, que ce soit un int optionnel » se termine souvent désagréablement.

@GetMapping("/api/v1/tasks")
public void findTasks(@RequestParam(required = false) int page) {
    // page : comment comptes-tu être null, mon ami ?
}

Il est plus correct soit d'utiliser un type wrapper (Integer), soit d'utiliser defaultValue (dans la section suivante), soit de reconnaître que page et size ne devraient généralement pas être « inconnus » — ils ont des valeurs par défaut.

Trois façons de décrire « peut être absent »

Pour ne pas se noyer dans les variantes, voici un tableau compact. Ce n'est pas « la meilleure pratique du monde », c'est pour que vous n'ayez pas à deviner pourquoi le code se comporte ainsi.

Ce que vous écrivez dans la méthode Ce qui arrive si le paramètre n'est pas envoyé Quand c'est pratique
@RequestParam(required = false) String q null Quand « pas de paramètre » = « pas de filtre »
@RequestParam(required = false) Integer size null Quand le nombre peut effectivement être inconnu
@RequestParam Optional<String> q Optional.empty() Quand vous voulez montrer explicitement « peut être absent » sans null

Optional a l'air élégant, mais pour les débutants, il ajoute souvent une charge cognitive supplémentaire : vous devez vous rappeler de q.isPresent(), q.orElse(...). C'est pourquoi dans un projet pédagogique, il est généralement plus simple de commencer avec required = false et un null compréhensible (oui, oui, nous utilisons null consciemment ; parfois c'est un outil d'ingénierie normal si vous le maîtrisez).

« Paramètre absent » et « paramètre vide » ne sont pas la même chose

Il y a une autre nuance qu'il est utile de garder à l'esprit, même si nous n'allons pas construire de logique complexe autour aujourd'hui.

Si le client appelle /api/v1/tasks et n'envoie pas assigneeName, le paramètre est tout simplement absent.

Si le client appelle /api/v1/tasks?assigneeName=, le paramètre est présent, mais la valeur est vide.

Pour un String, cela peut signifier "" (une chaîne vide), et c'est à vous de décider si cela signifie « pas de filtre » ou « filtre par nom vide » (généralement, la seconde option n'a pas de sens). Dans les API réelles, c'est important car ces nuances affectent la prévisibilité du contrat.

4. defaultValue : un comportement prévisible sans if manuels

Lorsque des paramètres « de service » de la liste (page, taille de l'affichage) apparaissent dans un endpoint, on souhaite presque toujours que le serveur fonctionne correctement même sans eux. C'est-à-dire que le client n'est pas obligé de se rappeler « par défaut page=0&size=20 ». Le serveur peut le spécifier lui-même — et cela fera partie du contrat.

defaultValue est une façon de dire : « Si le paramètre est absent (ou arrive vide), prends cette valeur ». Et c'est là qu'une petite magie sans magie opère : si vous spécifiez defaultValue, le paramètre cesse automatiquement d'être obligatoire car il a une valeur par défaut.

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public void findTasks(@RequestParam(defaultValue = "0") int page,
                      @RequestParam(defaultValue = "20") int size) {
    // page et size seront toujours des nombres :
    // - si le client a passé ?page=...&size=..., on les prendra,
    // - sinon, on utilisera les valeurs par défaut de defaultValue.
    //
    // Et oui, donc ici on peut utiliser les primitifs int en toute sécurité.
}

Cela présente plusieurs avantages pratiques.

Premièrement, vous pouvez utiliser les primitifs int car page et size auront toujours une valeur : soit celle de la requête, soit celle par défaut.

Deuxièmement, les valeurs par défaut deviennent explicites à la frontière de l'API. Cela améliore considérablement la lisibilité : vous ouvrez le contrôleur et comprenez immédiatement « comment le serveur se comporte si le client n'a rien envoyé ».

Troisièmement, vous êtes moins tenté d'écrire ce genre de chose :

int pageToUse = page == null ? 0 : page;

et de commencer à disperser les valeurs par défaut dans les services, les repositories et parfois même le frontend. La valeur par défaut fait partie du contrat. Il est donc logique de la placer à la frontière du contrat.

Il y a aussi une nuance : defaultValue est une chaîne. Spring substituera d'abord la chaîne, puis tentera de la convertir dans le type d'argument attendu. Si vous écrivez accidentellement "banana" alors que le type est int, « banane » ne se transformera pas en nombre. Même si vous insistez beaucoup.

5. Conception des paramètres de requête

Les paramètres de requête ne sont pas des « détails mineurs ». Ce sont les noms publics des champs de votre API. Les clients les écriront dans des fichiers .http, dans Postman, dans la documentation, et parfois dans du code (par exemple, dans une application mobile). C'est pourquoi ces noms ont deux caractéristiques importantes : la clarté et la stabilité.

La clarté l'emporte généralement sur la « brièveté pour la brièveté ». assigneeName se lit mieux que a. dueBefore est meilleur que d1. Nous concevons une API comme un contrat pour un humain, pas comme un championnat d'économie de caractères.

La stabilité signifie que vous ne voulez pas casser accidentellement le contrat simplement parce que vous avez renommé une variable dans la méthode du contrôleur. C'est pourquoi dans les équipes réelles, on suit souvent le principe : les noms des paramètres de requête doivent être « figés » aussi sérieusement que les noms des champs dans le JSON. Au niveau syntaxique, @RequestParam dispose pour cela de l'attribut name (ou value).

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@GetMapping("/api/v1/tasks")
public void findTasks(@RequestParam(name = "assigneeName", required = false) String assignee) {
    // Contrat (URL) : le paramètre s'appelle assigneeName
    // Code interne : la variable peut s'appeler n'importe comment (ici — assignee)
    // Cela aide à ne pas casser l'API lors du refactoring des noms dans le code Java.
}

J'ai délibérément montré des noms « différents » pour mettre en évidence que le contrat vit sa propre vie, indépendante du refactoring interne. Dans la pratique, il est bien sûr préférable de ne pas compliquer les choses et de nommer la variable comme le paramètre (assigneeName). Mais il est utile de savoir que le contrat peut être figé.

Orientation sur les paramètres pour GET /api/v1/tasks

Aujourd'hui, nous n'implémentons pas « tous les filtres et la pagination avancée », mais nous pouvons déjà choisir les noms et le style général pour ne pas créer de chaos.

Dans le cadre de Task Tracker API, les paramètres de requête naturels (au niveau du contrat) pour l'endpoint de liste ressemblent à ceci :

Paramètre Sens Exemple typique
status filtre par statut de tâche ?status=TODO
assigneeName filtre par exécutant ?assigneeName=Anna
q recherche textuelle (chaîne simple) ?q=docs
page numéro de page (généralement basé sur 0) ?page=0
size taille de page ?size=20

Même si certains de ces paramètres ne sont encore que « squelettiques », il est important de les maintenir dès maintenant dans un style cohérent : camelCase, noms prévisibles, sans abréviations soudaines « parce que c'est plus rapide ».

Et encore un signal pratique : si vous remarquez que la signature de la méthode commence à ressembler à un « annuaire téléphonique », ce n'est pas une raison pour souffrir. C'est une raison pour envisager les critères de recherche comme un seul objet — mais cela viendra un peu plus tard dans cette même journée, dans le cours sur @ModelAttribute. Aujourd'hui, nous examinons honnêtement @RequestParam comme un outil de base.

6. Exemple : GET /api/v1/tasks

Actuellement, nous avons un endpoint de liste GET /api/v1/tasks. Au départ, il pouvait être très simple : « renvoie toutes les tâches ». C'est normal pour une première étape. Mais il est maintenant temps de lui apprendre à accepter des filtres et des paramètres de service — de manière à ce que le contrôleur reste « mince » et le contrat lisible.

Voici un exemple de ce à quoi pourrait ressembler la méthode du contrôleur si nous ajoutons deux filtres (status, assigneeName) et deux paramètres de service (page, size). Notez qu'à l'intérieur de la méthode, nous ne faisons pas de logique « intelligente ». Nous nous contentons de recevoir l'entrée et de déléguer au service.

import com.example.tasktracker.domain.model.Task;
import com.example.tasktracker.domain.model.TaskStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;

@GetMapping("/api/v1/tasks")
public List<Task> findTasks(
        @RequestParam(defaultValue = "0") int page,          // paramètre de service : numéro de page
        @RequestParam(defaultValue = "20") int size,         // paramètre de service : taille de page
        @RequestParam(required = false) TaskStatus status,   // filtre : peut être absent (null => « ne pas filtrer »)
        @RequestParam(required = false) String assigneeName, // filtre : peut être absent
        @RequestParam(required = false) String q             // filtre/recherche : peut être absent
) {
    // Contrôleur : reçoit l'entrée HTTP et la transmet plus loin.
    // Important : ici on ne fait pas d'orchestration stream — on garde le contrôleur léger.
    return taskService.findTasks(page, size, status, assigneeName, q);
}

Oui, il y a déjà cinq paramètres. Est-ce beaucoup ? Pour une « première version d'un endpoint de liste » — un peu trop. Mais c'est précisément pour cela que nous apprenons : aujourd'hui, vous devez ressentir que les paramètres de requête sont normaux, mais que cette approche a une limite de confort. À ce stade, c'est utile comme pont : d'abord apprendre à accepter les paramètres de requête, puis apprendre à les regrouper en un seul critère.

Pour tester manuellement comment cela fonctionne, il est pratique d'avoir plusieurs requêtes au format .http :

### 1) Simple liste (sans filtres)
GET http://localhost:8080/api/v1/tasks
Accept: application/json
### 2) Filtre par statut
GET http://localhost:8080/api/v1/tasks?status=TODO
Accept: application/json
### 3) Filtre par statut + exécutant + « page »
GET http://localhost:8080/api/v1/tasks?status=IN_PROGRESS&assigneeName=Anna&page=1&size=10
Accept: application/json

Remarquez un autre détail qui vous épargne des nerfs : l'ordre des paramètres de requête n'a pas d'importance. Ce ne sont pas des arguments positionnels dans une méthode Java. Ce sont des paramètres nommés. Votre endpoint doit comprendre aussi bien ?page=1&size=10 que ?size=10&page=1.

Mini-implantation dans le service

La méthode du service peut être très simple : pour l'instant, nous n'implémentons pas la pagination réelle ni la recherche « intelligente », mais nous montrons au moins que les filtres peuvent être appliqués. Cela aide à sentir que @RequestParam n'est pas seulement une syntaxe, mais une véritable entrée dans le comportement de l'API.

Exemple d'implantation purement pédagogique (les filtres sont appliqués, page/size/q sont ignorés pour l'instant sans orchestration dramatique) :

import com.example.tasktracker.domain.model.Task;
import com.example.tasktracker.domain.model.TaskStatus;

import java.util.List;

public List<Task> findTasks(int page, int size, TaskStatus status, String assigneeName, String q) {
    // Pour l'instant exemple pédagogique : on ne met pas en œuvre la pagination ni la recherche q volontairement,
    // mais on montre comment les paramètres « optionnels » se transforment en filtrage.
    return tasks.stream()
            // status == null => filtre désactivé
            .filter(t -> status == null || t.status() == status)
            // assigneeName == null ou vide => filtre désactivé
            .filter(t -> assigneeName == null || assigneeName.isBlank()
                    || (t.assigneeName() != null && t.assigneeName().equalsIgnoreCase(assigneeName)))
            .toList();
}

Notez un style important : le contrôleur ne doit pas « absorber » ce code stream. Qu'il vive dans le service. Le contrôleur, c'est le contrat HTTP et l'orchestration, pas les règles métier et le filtrage.

7. Erreurs typiques avec @RequestParam

Erreur n°1 : transformer l'endpoint de liste en « quête obligatoire » de paramètres.
Il est très facile d'écrire une méthode avec cinq @RequestParam, d'oublier required = false et de se retrouver soudain avec une API qui ne fonctionne tout simplement pas sans ?status=. Pour les filtres, il est presque toujours plus correct de rendre le paramètre optionnel : l'absence de filtre doit signifier « ne pas filtrer », et non « casser la requête ».

Erreur n°2 : utiliser des primitifs (int, boolean) pour des paramètres qui peuvent être absents.
Un primitif, c'est « la valeur est toujours présente ». Or un paramètre de requête optionnel, c'est « la valeur peut ne pas arriver ». Si vous essayez de croiser ces deux affirmations, vous obtiendrez soit un comportement étrange, soit une erreur. Pour les nombres optionnels, utilisez Integer ; pour les paramètres avec une valeur par défaut évidente, utilisez un primitif + defaultValue.

Erreur n°3 : stocker les valeurs par défaut « n'importe où, mais pas dans le contrôleur ».
Si la valeur par défaut de size est 20, cela fait partie du comportement de l'API. Si vous placez la valeur par défaut tantôt dans le service, tantôt dans le contrôleur, et qu'ailleurs « on fait comme ça sur le front », vous obtiendrez un contrat impossible à comprendre sans fouilles archéologiques. defaultValue dans @RequestParam offre souvent la fixation la plus lisible des valeurs par défaut directement à la frontière.

Erreur n°4 : donner des noms peu clairs aux paramètres parce que « c'est plus rapide à taper ».
?a=Ann&s=TODO&p=1 semble économique, mais cette économie est généralement trompeuse : une semaine plus tard, personne ne se souvient de ce que signifie a. Le contrat public d'une API vit plus longtemps que votre humeur du jour. assigneeName, status, page, size — c'est moins excitant, mais au moins c'est clair.

Erreur n°5 : essayer de « cacher » les filtres dans le path plutôt que dans la requête.
Quand on a très envie d'un bel URI, on est tenté de faire /api/v1/tasks/status/TODO. Mais cela brise rapidement la scalabilité : ensuite apparaîtra /priority/HIGH, puis /assignee/Anna, et vous vous retrouvez à construire une hiérarchie imbriquée de filtres, alors que ce ne sont pas des ressources. Un filtre est un paramètre de requête vers une collection, pas l'adresse d'une ressource. Sa place est donc dans la chaîne de requête.

1
Mission
Spring REST & MVC, niveau 6, leçon 1
Bloqué
Recherche de notes par étiquette obligatoire
Recherche de notes par étiquette obligatoire
1
Mission
Spring REST & MVC, niveau 6, leçon 1
Bloqué
Liste de films avec genre optionnel et valeurs par defaut
Liste de films avec genre optionnel et valeurs par defaut
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION