1. API publique et objectif de la documentation
Si vous abordez REST Docs avec l’enthousiasme « documentons tout ce que le serveur renvoie », vous transformerez très vite la documentation en musée des hasards. Hier vous avez ajouté le champ debugInfo « pour un instant », aujourd’hui vous avez oublié de le retirer, demain le client l’a joyeusement utilisé, puis vous l’avez supprimé — et voilà une nouvelle saison de la série « Breaking Changes : The Reckoning ».
C’est pourquoi dans ContentHub nous commençons par la surface publique : ce que les clients externes consomment réellement (ou au moins d’autres parties de votre système). Il s’agit avant tout de GET /api/public/articles et GET /api/public/articles/{slug}, ainsi que des compagnons obligatoires de toute API publique : la pagination et les erreurs. Les détails internes du service, « comment on sélectionne les articles depuis la base », ne doivent pas être relatés dans REST Docs. La documentation est un contrat à la frontière HTTP, pas une autobiographie de votre @Service.
Pour ne pas tomber dans le « documentons tout », gardez à l’esprit un simple tableau de priorités (très peu romantique) :
| Ce que nous documentons | Pourquoi c’est important pour le client | Conséquences typiques si non documenté |
|---|---|---|
| Endpoints publics liste/détail | C’est l’entrée principale du système | Le client écrit le code « à vue de nez » et devine le contrat |
| Paramètres de requête de pagination/tri/filtre | Le client a besoin du format et des valeurs par défaut | Questions infinies : « pourquoi page commence à 0 ? », « sort comment ? » |
| Wrapper de page (métadonnées de page) | Le client construit l’interface utilisateur de pagination | L’interface se casse parce que totalPages a soudainement disparu |
| Contrat d’erreur (ApiProblem) | Les erreurs font aussi partie de l’API | Le client ne sait pas distinguer 404 et 409, n’affiche pas les violations |
L’idée de la leçon : nous documentons de manière économique, mais strictement selon le sens. C’est-à-dire que nous décrivons les champs, paramètres et structures importants, sans faire un instantané complet du JSON « comme la sérialisation nous l’envoie aujourd’hui ».
2. Paramètres de requête de GET /api/public/articles
La pagination et les filtres sont souvent perçus comme « ce sont des détails, le client devinera ». Ensuite vous voyez dans le chat de l’équipe : « Les gars, page commence à zéro ou à un ? », « size est limité ? », « le tri est par virgule ou par espace ? ». Et vous discutez de l’API non pas via la documentation, mais par la mémoire des gens. La mémoire des gens est une chose créative, mieux vaut ne pas lui faire confiance.
REST Docs est utile ici car il nous oblige à décrire honnêtement le contrat d’entrée : quels paramètres de requête existent, ce qu’ils signifient, lesquels sont facultatifs, quelles sont leurs valeurs par défaut. Et surtout, nous décrivons cela dans un test qui vérifie déjà que l’endpoint répond avec un statut 200 OK et renvoie un JSON de forme correcte.
Le squelette d’un test pour REST Docs ressemble généralement à ceci (nous restons dans une tranche MVC, car nous documentons la frontière HTTP, pas la base de données) :
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
// Test de tranche MVC : on ne monte que la couche web pour vérifier le contrat HTTP
@WebMvcTest(PublicArticleController.class)
@AutoConfigureRestDocs // Active la génération de snippets dans build/generated-snippets
class PublicArticleRestDocsTest {
@Autowired
MockMvcTester mvc; // Client simplifié pour exécuter des requêtes HTTP vers MockMvc
}
Ajoutons maintenant au test la documentation des paramètres de requête. Observez le style : d’abord on vérifie le statut, et seulement ensuite on « attache » document(...). C’est important psychologiquement : la documentation n’est pas l’objectif principal du test, elle est secondaire par rapport à la vérification du contrat.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentPublicArticlesQueryParams() {
// Nous effectuons un appel HTTP réel (pour le test) au contrôleur
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20&sort=publishedAt,desc"))
// 1) D’abord on vérifie que le chemin heureux est bien heureux
.hasStatusOk()
// 2) Ce n’est qu’ensuite qu’on génère les snippets de documentation
.apply(document("public-articles-list",
queryParameters(
// On décrit le contrat d’entrée : quels paramètres l’endpoint accepte
parameterWithName("page").optional().description("Numéro de page (indexé à partir de 0)"),
parameterWithName("size").optional().description("Taille de la page"),
parameterWithName("sort").optional().description("Tri au format field,(asc|desc)")
)
));
}
Deux nuances importantes qui n’apparaissent pas tout de suite, mais après le premier « combat avec la réalité ».
Premièrement, optional() ici n’est pas simplement « facultatif ». Cela signifie aussi que vous exécutez peut-être le test sans ce paramètre (par exemple, les valeurs par défaut page=0, size=20). Si vous oubliez optional(), REST Docs attendra toujours ce paramètre et se plaindra quand vous appelez l’endpoint sans sort. La documentation doit refléter le contrat réel : un paramètre facultatif est bien facultatif.
Deuxièmement, les descriptions dans description(...) doivent être écrites comme pour un client qui voit l’API pour la première fois. L’expression « param page » revient à écrire dans le code int x; // x. Formellement, le commentaire existe, mais son utilité pratique est nulle.
Pour rendre la documentation plus visuelle, il est souvent pratique d’avoir en tête un « tableau interne » pour les paramètres de pagination (dans REST Docs, il ne s’insère pas directement, mais il aide à formuler correctement les descripteurs) :
| Paramètre | Exemple | Signification | Question qu’il résout |
|---|---|---|---|
| page | 0 | numéro de page | « la numérotation commence à 0 ou à 1 ? » |
| size | 20 | taille de la page | « quelle est la valeur par défaut ? y a‑t‑il une limite ? » |
| sort | publishedAt,desc | tri | « comment spécifier le sens du tri ? » |
Si GET /api/public/articles a des filtres, par exemple category, il faut aussi le documenter — sinon le client cherchera le « paramètre magique » par méthode archéologique :
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.queryParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentCategoryFilter() {
// Scénario : le client filtre la liste d’articles par catégorie
assertThat(mvc.get().uri("/api/public/articles?category=java&page=0&size=20"))
.hasStatusOk() // Important : le filtre ne doit pas « casser » la réponse réussie
.apply(document("public-articles-list-with-category",
queryParameters(
// Les filtres font aussi partie du contrat public
parameterWithName("category").optional().description("Filtre par code de catégorie"),
// La pagination reste identique
parameterWithName("page").optional().description("Numéro de page (indexé à partir de 0)"),
parameterWithName("size").optional().description("Taille de la page")
)
));
}
Oui, les snippets vont se multiplier. Et c’est normal, tant qu’ils ne deviennent pas « un pour chaque éternuement ». Le but n’est pas la quantité de fichiers, mais que chaque snippet couvre un scénario stable distinct.
3. Réponse paginée : wrapper et content[]
La réponse paginée est l’endroit où « tout semble clair » se transforme en « pourquoi il est impossible de faire une pagination correcte côté client ». Le client a généralement besoin de deux choses : la liste des éléments (ici ce sont les articles) et les métadonnées de page (où nous sommes, combien de pages au total, quelle est la taille). Si les métadonnées ne sont pas documentées, chaque client commence à interpréter votre API à sa manière.
Dans ContentHub, nous ne renvoyons pas directement Page<Article> vers l’extérieur (c’est la bonne chose à faire) mais utilisons un wrapper comme PageResponse. REST Docs devrait donc idéalement montrer séparément la structure du wrapper et la structure des éléments de la liste. Il est important de ne pas tout mélanger dans un seul fieldWithPath("content[].something") sans compréhension.
Commençons par la description documentaire de base de PageResponse, sans entrer dans les champs des éléments. C’est comme étiqueter une boîte « page de résultats ici » et indiquer qu’elle contient content, page, size, totalElements, totalPages.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentPublicArticlesPageWrapper() {
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
.hasStatusOk() // On vérifie que la pagination fonctionne en principe
.apply(document("public-articles-list-page",
responseFields(
// On décrit le wrapper de page (métadonnées + liste)
fieldWithPath("content").description("Liste des articles sur la page"),
fieldWithPath("page").description("Numéro de page (indexé à partir de 0)"),
fieldWithPath("size").description("Taille de la page"),
fieldWithPath("totalElements").description("Nombre total d’articles trouvés"),
fieldWithPath("totalPages").description("Nombre total de pages")
)
));
}
Maintenant, documentons les éléments de content[]. Et c’est là que beaucoup commencent à faire un « instantané complet de la fiche article » avec tous les champs, y compris ceux dont le client n’a pas du tout besoin. Dans une liste publique, on se contente généralement de ce qui est réellement affiché dans la liste : slug, title, summary, publishedAt, quelque chose sur la catégorie.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentArticleCardInList() {
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
.hasStatusOk()
.apply(document("public-articles-list-content",
responseFields(
// Champs que le client affiche généralement dans la liste
fieldWithPath("content[].slug").description("Identifiant public de l’article"),
fieldWithPath("content[].title").description("Titre de l’article"),
fieldWithPath("content[].summary").description("Brève description"),
fieldWithPath("content[].publishedAt").description("Date de publication"),
// Minimum de métadonnées de page pour que l’interface puisse construire la pagination
fieldWithPath("page").description("Numéro de page (indexé à partir de 0)"),
fieldWithPath("size").description("Taille de la page")
)
));
}
Vous avez remarqué que dans ce snippet nous documentons à nouveau page et size. Cela ressemble à une duplication — et elle existe effectivement. Mais dans REST Docs, il y a un équilibre : il est parfois plus simple (et plus lisible) de dupliquer légèrement quelques champs que de construire des frameworks trop intelligents et des « snippets universels pour tous les cas ».
Si vous souhaitez être plus propre, vous pouvez extraire les FieldDescriptor communs dans une constante et les réutiliser. C’est particulièrement utile lorsque les mêmes champs de pagination ou de ApiProblem se répètent dans plusieurs snippets.
Autre point pratique : si votre API évolue et que vous ajoutez de nouveaux champs dans la réponse (par exemple content[].readingTimeMinutes), le strict responseFields(...) exigera de décrire chaque champ. C’est bien quand vous voulez un contrôle strict, mais il est parfois nécessaire de ne documenter qu’un minimum stable. Dans ce cas, une version « relaxée » est utile — relaxedResponseFields(...), qui ne plante pas en présence de champs non documentés.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.relaxedResponseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentOnlyStableFields() {
assertThat(mvc.get().uri("/api/public/articles?page=0&size=20"))
.hasStatusOk()
.apply(document("public-articles-list-relaxed",
relaxedResponseFields(
// On ne documente que le « noyau stable », le reste est ignoré
fieldWithPath("content[].slug").description("Identifiant public de l’article"),
fieldWithPath("content[].title").description("Titre de l’article")
)
));
}
Ce n’est pas de la « triche ». C’est un compromis conscient : vous choisissez ce qui est le plus important pour vous — l’exhaustivité stricte de la documentation ou la résistance à l’ajout de champs secondaires. Pour une API publique, il est généralement préférable de garder la documentation proche du contrat, et le contrat stable. Mais quand vous commencez à vivre avec REST Docs, le mode relaxed peut sauver votre santé mentale.
4. Endpoint de détail : GET /api/public/articles/{slug}
Il y a la tentation de documenter la liste et le détail dans un seul snippet, parce que « c’est un article, quelle différence ». La différence réside au moins dans le fait que le détail contient généralement plus de données, alors que la liste est plus compacte. Et si vous les mélangez dans une « documentation universelle de l’article », le client essaiera d’utiliser la liste comme détail ou l’inverse, et vous aurez le classique « pourquoi body est toujours null ? ».
C’est pourquoi nous créons un snippet séparé pour l’endpoint de détail. Et apparaît immédiatement un autre élément du contrat : le paramètre de chemin slug. Même si vous n’utilisez pas pathParameters(...) (existant dans REST Docs), le snippet doit montrer clairement que slug est la clé de la requête et ce qu’il faut y substituer.
Commençons par la documentation de la réponse du détail :
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentPublicArticleDetails() {
// Scénario : le client ouvre la page d’un article spécifique
assertThat(mvc.get().uri("/api/public/articles/java-testing-basics"))
.hasStatusOk() // On s’assure que l’article est accessible et que l’endpoint est vivant
.apply(document("public-article-details",
responseFields(
// Le détail est généralement plus riche que la carte de liste, mais on documente toujours selon le sens
fieldWithPath("slug").description("Identifiant public de l’article"),
fieldWithPath("title").description("Titre de l’article"),
fieldWithPath("summary").description("Brève description"),
fieldWithPath("body").description("Texte complet de l’article"),
fieldWithPath("publishedAt").description("Date de publication")
)
));
}
Ici aussi s’applique la règle d’économie : il n’est pas nécessaire de décrire « tous les champs du modèle domaine » s’ils ne font pas partie du contrat public stable. Par exemple, version (optimistic lock) ne concerne pas du tout le client de l’API publique. Ce qui est important pour vous dans la base et dans JPA ne l’est pas nécessairement pour le consommateur externe. La documentation vous aide à repérer ces mélanges : si la main vous démange de documenter version et authorUsername dans un endpoint public, c’est un bon moment pour vous arrêter et vous demander « le client a‑t‑il vraiment besoin de cela ? ».
Si vous souhaitez ajouter la documentation du paramètre de chemin (ce qui est généralement utile), vous pouvez le faire « par-dessus » le scénario testé :
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentSlugPathParam() {
assertThat(mvc.get().uri("/api/public/articles/java-testing-basics"))
.hasStatusOk()
.apply(document("public-article-details-path",
pathParameters(
// On documente la « clé » de la requête : ce qu’il faut précisément substituer dans l’URL
parameterWithName("slug").description("Slug de l’article publié")
)
));
}
Du point de vue du lecteur de la documentation, cela élimine un tas de questions : « est-ce un id ? », « est-ce un UUID ? », « est-ce un chemin lisible ? ». Nous disons clairement : c’est un slug, un identifiant public.
5. Erreurs de l’API : 404 et 400 pour la pagination
Une erreur dans l’API n’est pas « honteuse, cachons-la », mais un contrat au même titre qu’un 200 OK. De plus, les erreurs sont souvent plus importantes : le chemin heureux, le client peut le « gérer d’une manière ou d’une autre », mais s’il ne comprend pas ce que signifie errorCode, il ne pourra pas afficher un message correct à l’utilisateur et écrira simplement « Oups ».
Dans ContentHub, nous avons un format stable ApiProblem (style ProblemDetail) avec des champs comme type, title, status, detail, errorCode, et parfois violations. Cela signifie que nous pouvons et devons documenter ce payload. Une stratégie pratique consiste à créer des snippets séparés pour différentes classes d’erreurs : non trouvé (404), paramètres de pagination invalides (400), et éventuellement un « aperçu global du payload d’erreur ».
Documentons le 404 « article non trouvé ». Du point de vue du test, nous devons appeler l’endpoint avec un slug inexistant et obtenir un statut 404. Comment le service en est arrivé là est secondaire pour le test REST Docs ; cela se résout généralement par un stub via @MockitoBean, mais nous documentons non pas le mock, mais la réponse elle-même à la frontière HTTP.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentArticleNotFound() {
// Scénario : le client a demandé un article qui n’existe pas
assertThat(mvc.get().uri("/api/public/articles/missing-slug"))
.hasStatusNotFound() // Contrat : pour un article absent, on retourne 404
.apply(document("public-article-not-found",
responseFields(
// On documente le minimum utile pour le client afin de traiter l’erreur
fieldWithPath("title").description("Nom court de l’erreur"),
fieldWithPath("status").description("Statut HTTP"),
fieldWithPath("detail").description("Description lisible par un humain"),
fieldWithPath("errorCode").description("Code d’erreur stable")
)
));
}
Remarquez : nous ne documentons pas tous les champs de ApiProblem. C’est encore une économie consciente. Par exemple, instance et type peuvent être utiles, mais s’ils ne sont pas encore stabilisés ou diffèrent parfois, mieux vaut ne pas en faire des champs centraux. Vous pouvez les documenter plus tard ou les marquer comme facultatifs. Dans REST Docs, fieldWithPath(...).optional() fonctionne aussi pour les champs de réponse.
Maintenant, documentons le 400 Bad Request pour la pagination, si, par exemple, le client a passé une size incorrecte. Dans un monde idéal, ce sera une erreur de validation avec violations (par exemple, size doit être > 0 et <= 100). Et c’est exactement ce dont le client a besoin : une liste de violations.
import org.junit.jupiter.api.Test;
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
import static org.springframework.test.web.servlet.assertj.MockMvcTester.assertThat;
@Test
void shouldDocumentInvalidPaginationError() {
// Scénario : le client a passé une taille de page incorrecte
assertThat(mvc.get().uri("/api/public/articles?page=0&size=-1"))
.hasStatusBadRequest() // Contrat : paramètres invalides -> 400
.apply(document("public-articles-invalid-pagination",
responseFields(
// Champs de base de l’erreur
fieldWithPath("status").description("Statut HTTP"),
fieldWithPath("errorCode").description("Code d’erreur stable"),
// Violations de validation : le client peut mettre en évidence le champ et afficher le message
fieldWithPath("violations[].field").description("Champ en violation"),
fieldWithPath("violations[].message").description("Description de la violation")
)
));
}
Si violations n’est pas toujours présent (par exemple pour 404 il est absent), alors dans le snippet « général » on peut le marquer comme facultatif. Mais je ne créerais pas un seul « snippet d’erreur universel pour tous les cas » tout de suite, car il deviendrait trop flou. Il est plus pratique d’avoir 2-3 scénarios documentaires concrets : un pour not-found, un pour validation, et c’est tout.
Au final, on aboutit à une idée très saine : un endpoint peut normalement avoir deux snippets documentaires — le chemin heureux et le chemin d’erreur. Pour le client, c’est souvent encore plus important que « un champ de plus dans la réponse réussie ».
Pour fixer l’image, voici un mini-schéma de ce à quoi cela ressemble en pratique (dans la tête et dans le projet) :
flowchart TD
A["Test MVC (MockMvcTester)"] --> B["assertions : statut, JSON, headers"]
B --> C["document(...) : paramètres de requête / champs de réponse"]
C --> D["snippets générés"]
D --> E["Documentation API (assemblée à partir des snippets)"]
Un accent important : la documentation se tient sur les épaules d’un scénario testé. Nous ne dessinons pas le contrat à la main ; nous le faisons grandir à partir du test qui a déjà prouvé que le contrat est réellement ainsi.
6. REST Docs en situation : snippets et noms stables
Pour l’API publique de ContentHub, il est plus pratique de s’en tenir à des noms de snippets explicites ressemblant à des chemins : on voit immédiatement l’endpoint et le scénario. {method-name} reste une alternative fonctionnelle, mais ici il est plus important que l’arborescence generated-snippets raconte d’elle-même ce qui a été documenté.
Quand REST Docs apparaît pour la première fois dans un projet, l’émotion initiale est souvent : « Oh super, nous allons avoir de la documentation ! ». La seconde émotion une semaine plus tard : « Oh non, maintenant nous avons aussi de la documentation… ». Pour que REST Docs ne devienne pas une « taxe sur la vie », deux choses sont nécessaires : choisir raisonnablement ce qu’il faut documenter, et rendre les snippets réutilisables, mais sans magie.
Commençons par les noms des snippets. Le schéma le plus pratique est d’utiliser des noms qui se lisent comme un chemin : zone API + endpoint + scénario. Ainsi build/generated-snippets devient presque une arborescence auto-documentée. Par exemple :
| Nom du snippet | Description |
|---|---|
| public-articles-list | chemin heureux de la liste publique |
| public-article-details | chemin heureux du détail public |
| public-article-not-found | 404 sur le détail |
| public-articles-invalid-pagination | 400 sur la liste |
Ce n’est pas la seule approche correcte, mais elle aide à ne pas transformer les snippets en un ensemble « doc1 », « doc2 », « doc-final-final2 ».
Maintenant, la réutilisation. La manière la plus « sûre » de réutiliser n’est pas de cacher tout le test dans un DSL, mais d’extraire uniquement les morceaux répétables : par exemple, la description des champs de base de ApiProblem. Ainsi la documentation devient uniforme et le test reste lisible.
import org.springframework.restdocs.payload.FieldDescriptor;
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
// Petit helper : on garde les FieldDescriptor communs à un seul endroit
final class ApiProblemDoc {
static FieldDescriptor[] basic() {
return new FieldDescriptor[] {
// Contrat minimal d’erreur que nous considérons stable
fieldWithPath("title").description("Nom court de l’erreur"),
fieldWithPath("status").description("Statut HTTP"),
fieldWithPath("detail").description("Description de l’erreur"),
fieldWithPath("errorCode").description("Code d’erreur stable")
};
}
}
Ensuite vous utilisez cela dans le test et ajoutez les champs spécifiques au scénario, par exemple violations. Oui, il faudra concaténer les tableaux de descripteurs (manuellement ou avec un petit helper), mais l’idée est simple : le minimum commun vit à un seul endroit, et la « spécificité » reste dans le test.
Un autre principe qui évite les « snippets pour les snippets » : documentez ce qui est réellement stable et utile. Si un champ est calculé dynamiquement et que son sens n’est pas nécessaire au client, n’en faites pas un élément central de la documentation. REST Docs n’est pas une obligation de décrire chaque octet, mais une obligation de décrire le contrat.
Et pour finir : n’ayez pas peur qu’un même endpoint ait plusieurs snippets. Pour les clients, c’est normal : une même URL a différents scénarios et différentes réponses. Une documentation qui montre honnêtement « réponse réussie » et « erreur » est une documentation adulte. Une documentation qui fait comme si les erreurs n’existaient pas est comme un mode d’emploi de bouilloire sans section « que faire si elle ne s’allume pas ».
7. Erreurs typiques dans Spring REST Docs
Erreur n°1 : Documenter avant d’avoir vérifié le contrat avec des assertions.
Il est très facile de se laisser emporter par REST Docs et d’écrire document(...) comme objectif principal du test. Résultat : vous documentez un scénario qui n’est en réalité pas du tout vérifié. Cela transforme la documentation en une belle théorie. Le remède est simple et ennuyeux : d’abord le statut, les en-têtes et les champs significatifs de la réponse sont vérifiés par des assertions, et ensuite, seulement ensuite, on accroche document(...) par-dessus.
Erreur n°2 : Essayer de décrire dans la documentation « tout le JSON », y compris les champs aléatoires.
L’instantané « toute la réponse telle quelle » semble séduisant, mais il se casse très vite à la moindre refactorisation et génère une peur de modifier le contrat même là où c’est sûr. Dans REST Docs, il est préférable de documenter les champs stables et utiles, plutôt que de transformer la documentation en musée de la sérialisation. Si vous voulez une documentation partielle, utilisez l’approche relaxed ou documentez uniquement les parties importantes.
Erreur n°3 : Documenter uniquement le chemin heureux et ignorer le contrat d’erreur.
En pratique, les clients trébuchent plus souvent sur les erreurs que sur les réponses réussies. Si vous avez documenté le 200 OK, mais pas le ApiProblem pour 404 et 400, le client écrira quand même la gestion des erreurs « à vue de nez ». Ensuite votre équipe recevra des questions : « comment distinguer ARTICLE_NOT_FOUND de ACCESS_DENIED ? » et « où sont les violations ? ». Les erreurs font partie de l’API, et REST Docs est justement pratique pour vous forcer à le reconnaître.
Erreur n°4 : Mélanger la documentation de la pagination et celle des éléments de la liste de sorte que personne n’y comprenne rien.
Si dans un seul snippet vous essayez de décrire à la fois page/size/totalPages, et content[].slug/title/summary, et en plus les champs imbriqués de la catégorie, la documentation devient illisible. Il vaut mieux documenter le wrapper de pagination comme structure de page, et séparément la structure de l’élément (la carte article). Même si cela fait deux snippets, le lecteur vous en sera reconnaissant.
Erreur n°5 : Noms de snippets instables et « doc-final3 » dans le dépôt.
Au début, le nom semble un détail. Ensuite vous avez 40 snippets et vous passez plus de temps à chercher « comment s’appelait ce snippet sur not-found » qu’à faire du vrai travail. Un schéma de nommage stable (public/list/details/error) n’est pas de la bureaucratie, mais une navigation dans le projet quand les tests et la documentation deviennent nombreux.
GO TO FULL VERSION