CodeGym /Cours /Spring REST & MVC /REST : ressource, collection, représentation

REST : ressource, collection, représentation

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

1. Le vocabulaire REST et un langage commun

Si tu as déjà eu un débat en équipe sur « comment faire du REST correctement », tu connais cette douleur : une personne dit « ressource » et entend un objet JSON, une autre — une table en base, une troisième — un endpoint, et une quatrième — « n'importe quelle entité qui a un id ». La conversation ressemble à un dialogue de deux microservices sans contrat commun : tout le monde envoie des trucs, mais personne ne comprend personne.

Dans cette lektsiya, on fixe un ensemble minimal de termes qui permettra de concevoir Task Tracker API de façon consciente. Il nous faut distinguer ce qui existe dans le domaine (ressource), comment ça existe dans l'API comme ensemble (collection), comment ça se montre et se reçoit lors de l'échange (représentation) et ce qui n'existe que dans le contexte d'un parent (sous-ressource). Pas de philosophie, juste de la clarté ingénierie.

Pour que ce soit plus facile, garde à l'esprit une idée : le vocabulaire REST n'existe pas pour être « correct », mais pour répondre rapidement ensuite à des questions comme « c'est une entité séparée ou une partie d'une autre ? » et « ce JSON — c'est une ressource ou juste une représentation ? ».

Ressource : sens et identité

Quand on entend le mot « ressource » pour la première fois, on a très envie de le remplacer mentalement par « objet dans le code » ou « enregistrement en base ». C'est naturel : on a l'habitude de penser en termes de choses qu'on peut toucher. Mais en REST, une ressource — c'est avant tout une partie significative du domaine avec laquelle le client travaille, et elle a une identité stable (généralement — un identifiant).

L'essentiel ici, c'est que la ressource — c'est une « chose » au niveau du sens. Dans notre projet, une « tâche » est une ressource non pas parce qu'on aura une classe Task (on en aura une), et non pas parce qu'on aura une table tasks (pour l'instant non), mais parce que le client pense au système ainsi : « il y a des tâches, je veux en voir une, en modifier une, en supprimer une ». Autrement dit, pour le client, une « tâche » est une entité adressable séparée.

Un critère pratique très utile : une ressource — c'est ce à quoi on peut se référer avec la phrase « voilà cette chose » sans perdre le sens. « Cette tâche », « ce commentaire », « cette pièce jointe ». En revanche, « tri par priorité » — ce n'est pas une ressource, c'est un paramètre d'utilisation de la collection. « Statut » — ce n'est pas une ressource séparée dans notre projet, mais une partie de l'état de la tâche (du moins à cette échelle).

Mini-exemple Java (pas Spring, juste le sens de « il y a une identité ») :

import java.util.UUID;

public class IdExample {
    public static void main(String[] args) {
        // UUID joue ici le rôle d'identité stable de la ressource (taskId),
        // pas d'une "simple chaîne" (ce qui compte, c'est que c'est un identifiant adressable).
        String taskId = UUID.fromString("2f1d1b7a-3c8a-4f2b-9f2a-0e6f3c8b2d11").toString();

        // Pour le client, il est important que par cet id on puisse référencer "cette" tâche précise.
        System.out.println(taskId); // 2f1d1b7a-3c8a-4f2b-9f2a-0e6f3c8b2d11
    }
}

Peu importe où la tâche est stockée ici. Ce qui compte, c'est que la ressource « Task » dans notre projet sera adressable : elle a un id (une chaîne UUID selon les specs du projet), ce qui permet au client de dire : « donne-moi la tâche avec cet id ».

2. Collection et ressource individuelle

Quand on parle de « tâches » (tasks), il s'agit généralement de deux types de requêtes clients très différents en termes de sens. La première — « montre-moi la liste des tâches » (collection), la seconde — « montre-moi une tâche précise » (ressource individuelle). Si on ne fait pas cette distinction, l'API se perd : on essaie accidentellement d'appliquer à un seul objet les règles d'une collection, et à une liste — les règles d'une ressource individuelle.

Une collection — c'est un ensemble de ressources de même type. Point important : une collection — ce n'est pas juste un List<Task> en Java. En pensée REST, une collection est une partie du contrat externe : le client peut « parcourir » l'ensemble, y ajouter des éléments, y chercher ce dont il a besoin. Même si en interne on stocke les données comme on veut (tableau, map, fichier, « pour l'instant c'est en dur, c'est un projet pédagogique »), pour le client c'est quand même une collection de tâches.

Une ressource individuelle — c'est un élément précis de la collection. Et là d'autres attentes entrent en jeu : l'élément a un id précis, il a un état, et les opérations sur lui se font généralement « par adresse ».

On ne discute pas encore des règles sur « comment exactement les URI devraient se présenter » (ce sera demain), on fixe juste : ce sont deux objets de conversation différents.

import java.util.List;

public class CollectionVsItemExample {
    public static void main(String[] args) {
        // Chemin vers la collection : ici on "parle du multiple" (liste/recherche/ajout).
        String tasksCollection = "/tasks";

        // Chemin vers l'élément : ici on "parle d'une chose précise" par son identifiant.
        String oneTask = "/tasks/{taskId}";

        List<String> forms = List.of(tasksCollection, oneTask);

        // On voit clairement que ce sont deux objets de conversation différents : collection vs élément.
        System.out.println(forms); // [/tasks, /tasks/{taskId}]
    }
}

Même cet exemple simple est utile : /tasks — ce n'est pas « le chemin vers une tâche », mais le chemin vers la collection de tâches. Et /tasks/{taskId} — c'est déjà le chemin vers une tâche précise. Le domaine est le même, mais les questions du client sont différentes.

3. Représentation : comment la ressource se présente à l'extérieur

Le mot le plus sous-estimé dans le vocabulaire REST — « représentation ». Le débutant pense souvent : « Ben la ressource — c'est le JSON que je renvoie ». Tant que le projet est petit, ça semble fonctionner. Mais dès que la ressource commence à avoir une vraie vie (des champs différents, des scénarios différents, des formes de réponse différentes), on réalise : la ressource — c'est une chose, et sa forme JSON — une autre.

La représentation d'une ressource (representation) — c'est la forme des données dans laquelle la ressource est montrée au client ou reçue du client. Ça peut être JSON (dans notre cours c'est le format principal), mais en général, la représentation peut être autre chose : par exemple, une pièce jointe peut avoir des métadonnées JSON et un contenu binaire de fichier. On approfondira les fichiers plus tard, mais l'idée vaut la peine d'être fixée maintenant : representation — c'est « comment la ressource se présente lors de l'échange ».

Ce qui est important pour Task Tracker API : une même tâche peut avoir plusieurs représentations. Pour la liste des tâches, on préfère généralement montrer une « fiche courte » (id, title, status), et pour la vue détaillée — une « fiche complète » (plus la description, les tags, les délais, etc.). Ce n'est pas une « astuce subtile », mais un besoin normal du client.

Mini-exemple (on n'appelle pas encore ça des DTO et on ne plonge pas dans le design de contrat — on fixe juste l'idée de représentations différentes) :

import java.time.LocalDate;

// "Sens ressource" : la tâche en tant qu'entité du domaine (forme interne/riche).
record Task(String id, String title, String description, LocalDate dueDate) {}

// "Représentation" : forme simplifiée pour un scénario précis (par exemple, liste de tâches).
record TaskSummaryView(String id, String title) {}

public class RepresentationExample {
    static TaskSummaryView toSummary(Task task) {
        // Mapping vers la représentation : on choisit consciemment quels champs exposer.
        return new TaskSummaryView(task.id(), task.title());
    }
}

Task ici — c'est le « sens ressource » (la tâche), et TaskSummaryView — l'une de ses représentations. En pensée REST, on apprend à distinguer « ce qui existe » de « comment on le montre ».

4. Ressource et représentation : exemple Task Tracker

La confusion « ressource = JSON » surgit généralement au pire moment : quand on veut ajouter un champ interne, changer le format de réponse ou cacher quelque chose au client. En interne, l'application peut stocker plus de données qu'elle n'en expose à l'extérieur, et c'est normal. Vers l'extérieur doit sortir un contrat contrôlé, tandis que la ressource en tant qu'entité sémantique reste la même.

Ancrons cette distinction sur un seul domaine. Prenons la tâche comme ressource et réfléchissons à comment elle peut se présenter à l'extérieur.

Concept Ce que ça signifie Exemple dans Task Tracker Ce qu'il faut retenir
Ressource Entité significative du domaine « Tâche » Pas nécessairement une classe Java ni une table
Représentation Forme des données « lors de l'échange » « Fiche courte de tâche » pour la liste, « fiche détaillée » pour la vue Il peut y avoir plusieurs représentations
Identité Ce qui fait que la ressource est « celle-là » taskId (chaîne UUID) L'identifiant stabilise la conversation du client avec l'API

Encore un petit exemple de code pour ressentir comment le modèle interne peut être plus riche que la forme externe. On ajoute exprès un champ « technique » et on ne l'inclut pas dans la représentation :

import java.time.Instant;

// Modèle interne : contient des données techniques qui n'ont pas à être publiques.
record TaskInternal(String id, String title, String internalNote, Instant createdAt) {}

// Représentation publique : contrat contrôlé pour le client.
record TaskPublicView(String id, String title, Instant createdAt) {}

public class InternalVsPublicExample {
    static TaskPublicView toPublic(TaskInternal task) {
        // Important : internalNote n'est volontairement pas exposé — c'est de "l'hygiène contractuelle".
        return new TaskPublicView(task.id(), task.title(), task.createdAt());
    }
}

Dans un vrai projet, internalNote peut être n'importe quoi : un marqueur technique, un flag interne de migration, des détails de stockage dans un système de fichiers, etc. L'essentiel : l'existence d'un champ en interne ne signifie pas qu'il doit figurer dans le contrat externe. Et c'est précisément cette distinction entre ressource et représentation qui sauve ensuite l'API du chaos.

5. Sous-ressource : objet dans le contexte d'un parent

On arrive maintenant au mot qui embrouille le plus souvent les débutants : « sous-ressource ». Une sous-ressource — c'est aussi une ressource, mais qui existe naturellement dans le contexte d'une autre ressource, et dont le cycle de vie est le plus souvent lié au parent. Ce n'est pas un « morceau de JSON dans un JSON », mais une entité séparée, juste pas autonome au niveau supérieur de notre modèle.

Dans Task Tracker API, un commentaire est quasi impossible à discuter dans le vide. La phrase « supprime le commentaire c-123 » sans préciser la tâche sonne bizarre : c'est quel commentaire, il concerne quoi ? Pareil pour une pièce jointe : une pièce jointe « toute seule » ne sert quasiment à rien à l'utilisateur, il en a besoin « comme pièce jointe d'une tâche ».

C'est justement le sens principal d'une sous-ressource : le contexte parent est important pour le sens. C'est pour ça qu'on lit comments et attachments comme des sous-ressources de la tâche.

Exemple au niveau du modèle (encore une fois — pas Spring ni le design final, juste le sens : « il y a un lien avec l'id parent ») :

import java.time.Instant;

// La sous-ressource (comment) a sa propre identité (id),
// mais son sens/cycle de vie est lié à la ressource parent (taskId).
record Comment(String id, String taskId, String authorName, String text, Instant createdAt) {}

public class SubresourceExample {
    static boolean belongsToTask(Comment comment, String taskId) {
        // Vérification du lien avec le parent — dans l'API ça se traduira naturellement par l'URI,
        // et dans le modèle souvent par le champ taskId.
        return comment.taskId().equals(taskId);
    }
}

La présence de taskId dans le commentaire souligne l'idée : le commentaire a sa propre identité (id), mais vit « sous » la tâche (taskId). C'est un modèle qui se transpose ensuite très naturellement sur la carte de ressources de l'API.

6. Sous-ressources et lookup : comments, attachments, tags

Quand on commence à concevoir une API, il est très facile de rendre tout « également important » et de transformer le domaine en un zoo CRUD infini. Chez le débutant, ça ressemble généralement à ça : « Puisqu'il y a des commentaires — il faut un CRUD complet au niveau supérieur pour comments. Puisqu'il y a des pièces jointes — encore un. Puisqu'il y a des tags — pareil ». Et deux jours plus tard, on n'a plus un Task Tracker, mais une collection de contrôleurs disparates.

Dans Task Tracker API, il vaut mieux distribuer les rôles d'emblée. Task — la ressource principale. Comment et Attachment — des sous-ressources auxiliaires de la tâche : elles ont leurs propres id, mais leur sens tient à la tâche parente. Le client a généralement besoin non pas d'un « commentaire en général », mais d'un commentaire d'une tâche précise ; non pas d'un « fichier du monde », mais d'une pièce jointe d'une tâche précise.

Avec les tags, c'est différent. Dans le cadre du cours, les tags — ce sont des valeurs qui vivent dans les tâches et aident à les chercher et les classer. Donc tags s'envisage mieux comme une collection de valeurs de type lookup : le client a besoin d'une liste des variantes disponibles, pas d'un module de gestion des tags à part entière.

Petit exemple pour faire ressentir « les tags comme valeurs » en pratique :

import java.util.Set;
import java.util.TreeSet;

public class TagsExample {
    public static void main(String[] args) {
        // En tant que "valeurs", les tags s'avèrent pratiques à normaliser/dédupliquer.
        // Ici pour la démo on utilise un ensemble avec comparaison insensible à la casse.
        Set<String> tags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);

        tags.add("backend");
        tags.add("Backend"); // Doublon avec une capitalisation différente.

        // Idée : si les tags sont des valeurs, leur unicité/normalisation compte plus que leur cycle de vie CRUD.
        System.out.println(tags); // [backend]
    }
}

Cette distinction suffit déjà pour ne pas confondre trois rôles : ressource principale, sous-ressource, et valeurs de type lookup.

7. Schéma de référence du modèle de ressources

Quand ces mots sont bien posés, la surface du projet se lit sans hésitation :

  • task — la ressource principale, avec laquelle le client travaille comme une entité adressable séparée ;
  • comments et attachments — des sous-ressources de task, car leur sens tient au parent ;
  • tags — une collection de valeurs de type lookup, pas un univers CRUD parallèle ;
  • les représentations vivent à côté des ressources et n'ont pas à coïncider avec le modèle interne.

Ce schéma suffit pour passer à la question ingénierie suivante : comment GET, POST, PUT, PATCH, DELETE s'appliquent à la collection et à une ressource individuelle ? Autrement dit, on a maintenant le vocabulaire — il faut maintenant la grammaire des opérations.

8. Erreurs typiques avec les termes

Erreur n°1 : appeler ressource n'importe quel fragment JSON.
Souvent le débutant voit un champ status ou priority dans une tâche et dit : « oh, c'est une ressource ». Il vient alors des idées étranges comme « faisons des /statuses et des /priorities et externalisons tout en entités séparées ». Ça n'améliore généralement pas l'API, ça ne fait que la gonfler. Une ressource — ce n'est pas « n'importe quelle partie des données », mais une unité significative du domaine avec laquelle le client interagit vraiment comme avec une « chose ».

Erreur n°2 : confondre collection et ressource individuelle, puis s'étonner d'opérations bizarres.
Si on pense à une « liste de tâches » comme à « une tâche, mais en plusieurs exemplaires », l'API produit des hybrides : par endroit on essaie de « supprimer la liste », par endroit de « mettre à jour toutes les tâches en une requête », par endroit de « récupérer une tâche, mais sans id ». La pensée correcte est plus simple : la collection répond à « lesquelles existent ? », et la ressource individuelle — « c'est quoi cette chose précise ? ».

Erreur n°3 : croire que ressource et représentation doivent coïncider un pour un.
Ça mène au monstre TaskDtoForEverything, qui sert à la fois pour la liste, pour les détails, pour la création, pour la mise à jour, et qui contient en plus des champs inattendus que le client « ne doit pas toucher, mais on les lui a fournis, espérons qu'il ne les touche pas ». Dans une bonne API, la ressource est stable par le sens, et les représentations sont choisies selon les scénarios du client. On le formalise terminologiquement aujourd'hui, et plus tard on le concrétisera en pratique de DTO et de contrats.

Erreur n°4 : faire des sous-ressources de tout et n'importe quoi.
Parfois il semble que puisque Task a un assigneeName, alors « utilisateur » est une sous-ressource, donc /tasks/{id}/assignee. Puis apparaissent /tasks/{id}/priority, /tasks/{id}/status, /tasks/{id}/title, et l'API devient un ensemble de « boutons » pour chaque champ individuel. Une sous-ressource — c'est une entité séparée avec un sens et son propre cycle de vie, pas « n'importe quel champ JSON ».

Erreur n°5 : gonfler les éléments secondaires à l'échelle de la ressource principale.
Dans notre projet, les tags sont utiles, mais ils ne constituent pas une « entité principale au niveau de Task ». Transformer Tag en module CRUD complet génère un gros volume de code et de discussions qui n'apportent pas de nouvelles connaissances sur le contrat REST, mais augmentent juste la surface du projet. Dans un domaine pédagogique, savoir s'arrêter à temps — c'est une compétence qui vaut bien l'écriture d'un autre contrôleur.

1
Mission
Spring REST & MVC, niveau 3, leçon 1
Bloqué
Ressource et sa représentation
Ressource et sa représentation
1
Mission
Spring REST & MVC, niveau 3, leçon 1
Bloqué
Collection, ressource, sous-ressource et lookup
Collection, ressource, sous-ressource et lookup
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION