CodeGym /Cours /Spring REST & MVC /consumes et produces dans Spring MVC

consumes et produces dans Spring MVC

Spring REST & MVC
Niveau 7, Leçon 3
Disponible

1. Fixation explicite du format dans une API orientée JSON

Au début, on pourrait croire que consumes/produces est une bureaucratie du genre « cochez que vous avez coché ». Mais dans la conception d’API, les détails des en‑têtes sont comme la signature d’un contrat : quand tout va bien, on ne les remarque pas, et quand tout va mal, on découvre soudain que ce sont eux qui tranchent le litige. Si nous ne fixons pas le format explicitement, le client peut envoyer un corps de requête dans un format inattendu (ou avec un Content-Type inattendu), et nous passerons ensuite beaucoup de temps à comprendre pourquoi le contrôleur « semble ne pas être appelé ». De même, si le client demande un format via Accept alors que le serveur en renvoie un autre, cela se transforme en un problème flottant où chacun est certain que ce n’est pas de sa faute.

Dans notre cours, nous créons délibérément une API orientée JSON, c’est‑à‑dire que le format principal est application/json. Cela ne signifie pas qu’on peut cesser de réfléchir aux types de médias. Au contraire : puisque nous choisissons consciemment un seul format, il est logique de le déclarer lui aussi consciemment dans le contrat des endpoints. Ainsi, tout développeur, en ouvrant le contrôleur, comprend en 10 secondes : « ah, cet endpoint accepte du JSON et renvoie du JSON », et non « eh bien, c’est probablement ainsi par défaut ». Cela vous fait gagner du temps en débogage, en explications avec les collègues et en échanges avec des clients qui envoient du text/plain parce que “c’est plus pratique”.

Content-Type et Accept : les en‑têtes du format

Pour utiliser correctement consumes et produces, rappelons‑nous rapidement le sens des deux en‑têtes clés. Ce n’est pas « une théorie tirée de la RFC », mais littéralement le langage par lequel le client et le serveur s’accordent sur la façon de lire et d’écrire le body. Content-Type concerne le corps de la requête : le client dit au serveur « voici mon body, il est dans tel format ». Accept concerne le corps de la réponse : le client dit au serveur « je veux recevoir la réponse dans tel format ».

Dans une API orientée JSON, cela ressemble généralement à ceci : le client envoie Content-Type: application/json et ajoute souvent Accept: application/json. Si Accept n’est pas spécifié, certains clients se comportent par défaut comme s’il y avait */* (autrement dit « n’importe quel format me convient »), et le serveur renvoie ce qu’il sait faire. Mais dans la vraie vie, la présence d’un Accept explicite est très utile : il transforme un « espoir » en contrat.

Un petit tableau pour clarifier les idées (et pour ne pas les confondre automatiquement) :

Entité Où vit‑elle Qui la définit Sur quoi porte‑t‑elle Exemple
Content-Type en‑tête HTTP de la requête client « quel est le format de mon corps de requête » Content-Type: application/json
Accept en‑tête HTTP de la requête client « dans quel format je veux le corps de la réponse » Accept: application/json
consumes annotation de mapping dans Spring MVC serveur (nous) « quels Content-Type je suis prêt à accepter dans le corps de la requête » consumes = "application/json"
produces annotation de mapping dans Spring MVC serveur (nous) « quels formats je suis prêt à fournir dans le corps de la réponse » produces = "application/json"

Remarquez le petit « piège pour débutants » : Content-Type et Accept sont des en‑têtes de la requête, tandis que consumes/produces sont des « règles » côté serveur selon lesquelles Spring choisira votre méthode de contrôleur ou répondra « non, nous ne sommes pas d’accord ». C’est précisément ce comportement dont nous avons besoin aujourd’hui.

2. consumes et produces comme filtre du contrat

Si on explique de façon très humaine, consumes et produces font partie des règles de correspondance d’un endpoint. Spring ne regarde pas seulement le chemin et la méthode HTTP (POST /api/v1/tasks), il vérifie aussi : « cette requête correspond‑elle au format ? ». Et là, consumes/produces imposent une discipline : si la requête arrive avec un Content-Type que vous n’acceptez pas, Spring peut renvoyer 415 Unsupported Media Type. Si la requête arrive avec un Accept que vous ne pouvez pas satisfaire, Spring peut renvoyer 406 Not Acceptable.

Il est important de bien comprendre le moment : dans ces cas, votre code à l’intérieur de la méthode du contrôleur peut ne jamais être exécuté. Et c’est normal. Ce n’est pas « Spring est cassé ». Cela signifie que la requête ne correspond pas au contrat au niveau de la trame HTTP. Votre logique métier n’est pas en cause : vous ne savez tout simplement pas comment lire le body, ou vous ne pouvez pas fournir la réponse telle que le client la demande.

On peut imaginer cela comme un tourniquet dans le métro. Vous avez un ticket, vous passez ; pas de ticket, vous ne passez pas. Et le tourniquet n’est pas obligé de discuter de questions philosophiques « pourquoi », sa tâche est de filtrer le flux rapidement et clairement. consumes et produces sont ce même tourniquet, mais pour le format du body.

Un schéma d’un niveau « suffisant pour un junior » peut ressembler à ceci :

flowchart TD
    A[Requête HTTP] --> B[Spring MVC : recherche de la méthode handler appropriée]
    B --> C{Le chemin et la méthode correspondent ?}
    C -->|non| X["404 / aucun gestionnaire"]
    C -->|oui| D{consumes correspond au Content-Type ?}
    D -->|non| E[415 Unsupported Media Type]
    D -->|oui| F{produces correspond à Accept ?}
    F -->|non| G[406 Not Acceptable]
    F -->|oui| H[Appel de la méthode du contrôleur]

Nous n’examinons pas encore en détail la mécanique interne – « qui lit exactement le body ? » et « quel composant gère le JSON ? ». Mais il est utile d’avoir à l’esprit que consumes/produces font partie du filtrage de la requête avant votre logique applicative, et que cela concerne directement le contrat de l’API.

3. Fixons le type de média dans le contrôleur et les méthodes

produces au niveau de TaskController

Passons maintenant à une chose concrète dans notre Task Tracker API : fixons au niveau du contrôleur qu’il renvoie du JSON. Pourquoi au niveau du contrôleur ? Parce qu’ainsi nous n’écrirons pas la même chose dans chaque méthode, et le contrat deviendra lisible « de l’extérieur » : on ouvre la classe, on comprend immédiatement qu’il s’agit de application/json.

Imaginez que le contrôleur est un dossier contenant des endpoints. Si dans ce dossier presque toutes les réponses sont en JSON, il est logique de l’écrire une fois sur le dossier, pas sur chaque fichier à l’intérieur. Pour un contrôleur orienté JSON, c’est d’autant plus pratique qu’un autre produces n’est nécessaire que là où la réponse elle-même a un format différent. Mais aujourd’hui nous sommes dans un monde orienté JSON, et il est important de le rendre explicite.

Structure minimale d’un contrôleur avec produces :

package com.example.tasktracker.api.controller;

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController // Nous disons à Spring : c'est un contrôleur REST, les réponses iront dans le body
@RequestMapping(
        path = "/api/v1/tasks", // Chemin de base pour tous les endpoints des tâches
        produces = MediaType.APPLICATION_JSON_VALUE // Par défaut, nous renvoyons du JSON
)
public class TaskController {

    // Les méthodes handler (GET/POST/PUT/DELETE) pour travailler avec les tâches seront ici
}

Nous faisons ici deux choses importantes. Premièrement, nous fixons le chemin de base /api/v1/tasks, pour que les méthodes internes soient plus compactes. Deuxièmement, nous disons à Spring MVC : « tout ce qui sort de ce contrôleur comme corps de réponse sera fourni en JSON ». Et cela ne concerne pas seulement « pour que ce soit joli pour le client ». Il s’agit de rendre notre API prévisible : même si vous renvoyez directement un objet, Spring définira le bon Content-Type dans la réponse.

consumes pour @RequestBody

L’étape logique suivante est de fixer le format d’entrée pour les opérations où nous lisons le corps de la requête. Dans notre projet, il s’agit de la création et de la mise à jour des tâches. Lorsque la signature d’une méthode contient @RequestBody, nous disons par essence : « le client envoie un body, nous le lisons ». Il est donc utile de préciser immédiatement : « nous ne lisons que du JSON ». C’est exactement ce qu’exprime consumes = application/json.

Pourquoi n’est‑ce pas simplement une « garniture » ? Parce que sinon le client peut envoyer un body sans Content-Type (ou avec text/plain), et vous serez surpris que Spring ne veuille pas le transformer en votre objet Java. Avec consumes, tout devient clair : notre accord est le JSON, et si le client apporte autre chose, la requête n’est pas acceptée.

Exemple de POST de création de tâche :

import com.example.tasktracker.api.dto.request.TaskCreateRequest;
import java.net.URI;

import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE) // Nous n'acceptons que du JSON dans le body de la requête
public ResponseEntity<Task> create(@RequestBody TaskCreateRequest request) {
    // request provient du body JSON de la requête et a déjà été analysé par Spring

    Task created = taskService.create(request.getTitle(), request.getDescription());

    // Pour un 201 Created, bonne pratique : renvoyer l'en-tête Location avec l'URL de la ressource créée
    URI location = URI.create("/api/v1/tasks/" + created.getId());

    return ResponseEntity
            .created(location) // HTTP 201 + en-tête Location
            .body(created);    // Corps de la réponse (JSON, car produces est défini sur le contrôleur)
}

Remarquez le détail : nous n’écrivons pas produces ici, car nous l’avons déjà fixé au niveau du contrôleur. Si cette lecture vous convient, vous pouvez aussi l’écrire sur la méthode, mais vous commencerez à répéter la même chose. Dans un projet d’apprentissage, il est préférable de maîtriser la répétition pour que le code ne ressemble pas à un « papier peint d’annotations ».

Pour PUT et PATCH, la même approche s’applique : si @RequestBody est présent, alors la méthode a un format d’entrée, et nous pouvons le définir explicitement :

import com.example.tasktracker.api.dto.request.TaskUpdateRequest;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;

@PutMapping(path = "/{taskId}", consumes = MediaType.APPLICATION_JSON_VALUE) // Nous lisons du JSON depuis le body
public ResponseEntity<Task> replace(@PathVariable String taskId,
                                    @RequestBody TaskUpdateRequest request) {
    // taskId provient de l'URL, request provient du body JSON
    Task updated = taskService.replace(taskId, request.getTitle(), request.getDescription());

    return ResponseEntity.ok(updated); // 200 OK + body JSON (produces sur le contrôleur)
}

En revanche, DELETE ne nécessite généralement pas consumes : une suppression n’a généralement pas de body de requête. Avec produces, il faut aussi être prudent, car 204 No Content est un succès sans body. Vous pouvez avoir un produces commun sur le contrôleur, cela fonctionnera, mais il est utile de garder à l’esprit que 204 signifie « pas de body », donc le format du body n’est plus aussi important que le statut.

4. Requêtes client et 415/406

Voici un point utile qui permet d’économiser beaucoup d’heures de débogage. Lorsque vous avez ajouté consumes/produces, certaines requêtes peuvent commencer à renvoyer 415/406, et un développeur débutant perçoit souvent cela comme « quelque chose s’est cassé dans mon contrôleur ». Mais non : vous avez simplement ajouté une discipline contractuelle, et désormais les requêtes doivent s’y conformer.

Exemple de requête correcte de création de tâche en style .http :

# Requête correcte : Content-Type et Accept sont alignés avec le contrat du contrôleur
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
Accept: application/json

{
  "title": "Fix login bug",
  "description": "Users cannot sign in after last deploy"
}

Si vous envoyez accidentellement un body sans Content-Type ou avec un type incorrect, par exemple :

# Content-Type incorrect : le serveur attend du JSON, mais a reçu text/plain
POST http://localhost:8080/api/v1/tasks
Content-Type: text/plain
Accept: application/json

{"title":"Fix login bug"}

alors Spring peut renvoyer 415 Unsupported Media Type. Et il est important de comprendre : ce n’est pas une erreur de « votre logique métier ». C’est une erreur de niveau « nous ne nous sommes pas mis d’accord sur la façon de lire le body ». Votre taskService.create(...) n’est pas en cause dans ce scénario et, très probablement, n’a même pas été appelé.

Exemple avec Accept. Supposons que le cliente dise « je veux du texte » :

# Accept incorrect : le client demande text/plain, alors que le serveur, par contrat, renvoie du JSON
GET http://localhost:8080/api/v1/tasks
Accept: text/plain

Si le contrôleur a produces = application/json, Spring peut renvoyer 406 Not Acceptable. C’est la situation « le serveur ne peut fournir que du JSON, mais le client demande text/plain ». Dans la vraie vie, les navigateurs ne feront généralement pas cela, mais certains outils et intégrations peuvent le faire.

Et c’est là qu’apparaît une habitude d’ingénierie très utile : lorsque vous voyez 415 ou 406, vous regardez d’abord non pas le service ni le repository, mais les en‑têtes de la requête. C’est l’endroit où le problème se cache le plus souvent.

5. Comment éviter la « forêt d’annotations »

Parlons maintenant de la discipline dans le code. Il est très facile de tomber dans deux extrêmes. Le premier extrême : ne rien fixer du tout et espérer que « par défaut, c’est du JSON ». Le second extrême : écrire consumes et produces absolument partout, en plus avec des chaînes, parfois avec des fautes de frappe, puis déboguer héroïquement pourquoi la moitié des endpoints ne correspondent pas.

La voie saine pour un projet d’apprentissage ressemble généralement à ceci : on fixe produces au niveau du contrôleur (ou du @RequestMapping commun), de sorte que l’ensemble des endpoints soit dans un format unique. Quant à consumes, on ne le place que là où on lit effectivement le body via @RequestBody. Cela rend le contrat explicite sans transformer le code en rideau d’annotations.

Il est également important d’éviter les chaînes magiques. Écrire "application/json" manuellement partout, c’est comme écrire « localhost » à la main dans chaque ligne de code : une erreur et tout échoue sans raison. Utilisez donc les constantes Spring :

import org.springframework.http.MediaType;

// Endroit unique où nous fixons les types de médias de notre API
public final class ApiMediaTypes {

    // JSON est le format de base de notre API orientée JSON
    public static final String JSON = MediaType.APPLICATION_JSON_VALUE;

    private ApiMediaTypes() {
        // Nous interdisons la création d'instances : c'est une classe utilitaire avec des constantes
    }
}

Ainsi, dans le contrôleur, on peut écrire de façon compacte et sans faute de frappe :

import static com.example.tasktracker.api.ApiMediaTypes.JSON;

import org.springframework.web.bind.annotation.PostMapping;

@PostMapping(consumes = JSON)
public ResponseEntity<Task> create(@RequestBody TaskCreateRequest request) {
    // Le reste du code de la méthode (création de la tâche, construction de la réponse, etc.)
}

Ce n’est pas une technique obligatoire, mais elle illustre bien l’idée générale : le format fait partie du contrat, et on souhaite garder le contrat propre et uniforme, plutôt que par copier‑coller.

6. Erreurs typiques lors du travail avec consumes et produces

Erreur n°1 : confusion entre « qui répond de quoi » entre Content-Type, Accept, consumes, produces.
Situation très fréquente : le développeur définit produces en pensant que cela signifie « nous acceptons ceci », ou définit consumes en pensant que cela signifie « nous renvoyons ceci ». Résultat : l’endpoint commence soudain à renvoyer 415/406, et la personne cherche le problème dans le service. Vérification simple : consumes concerne ce que le serveur lit (comment digérer le corps de la requête), tandis que produces concerne ce que le serveur écrit (comment former le corps de la réponse).

Erreur n°2 : mettre mécaniquement consumes sur un GET qui n’a pas de corps de requête.
Au niveau du sens, c’est simplement du bruit : vous fixez le format de quelque chose qui n’existe pas. Au niveau comportemental, on peut parfois obtenir des effets inattendus, surtout si un client envoie soudain un Content-Type là où vous ne l’attendez pas. Dans un projet d’apprentissage, il est préférable de s’en tenir à une règle simple : consumes n’apparaît que là où il y a @RequestBody.

Erreur n°3 : écrire "application/json" à la main dans vingt endroits et se tromper une fois.
Une faute de frappe comme "applicaton/json" est drôle jusqu’au moment où vous avez passé 40 minutes à déboguer et que vous réalisez que l’endpoint « n’est pas trouvé » parce que consumes ne correspondait pas. Utilisez MediaType.APPLICATION_JSON_VALUE ou votre propre constante, et vous éliminez toute une classe d’erreurs.

Erreur n°4 : considérer que 415/406 sont des « erreurs de mon code dans la méthode du contrôleur ».
En réalité, cela signifie souvent que vous n’êtes jamais arrivé jusqu’à la méthode. Le service peut être parfait, le repository génial, et @RequestBody magnifique, mais si la requête est arrivée avec un Content-Type incorrect ou un Accept incompatible, le tourniquet ne laisse pas passer. L’habitude de regarder les en‑têtes de la requête dans ces cas‑là est une véritable économie de temps d’ingénierie adulte.

Erreur n°5 : définir produces = application/json sur un endpoint qui renvoie 204 No Content et s’attendre à un « JSON vide ».
204 signifie « pas de body », pas « body vide ». C’est une différence subtile mais très importante. Si vous souhaitez renvoyer {} ou un objet de confirmation, ce sera 200 OK avec un body, pas 204. Ne faites pas semblant que 204 a un format de body : formellement, il ne doit pas y avoir de body.

1
Mission
Spring REST & MVC, niveau 7, leçon 3
Bloqué
Endpoint JSON-only avec `consumes` et `produces`
Endpoint JSON-only avec `consumes` et `produces`
1
Mission
Spring REST & MVC, niveau 7, leçon 3
Bloqué
JSON-only `GET` et réponse `406 Not Acceptable`
JSON-only `GET` et réponse `406 Not Acceptable`
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION