Chapitre 825 minDébutant
Documenter une API REST avec OpenAPI 3.1
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- lire et écrire un document OpenAPI 3.1 ;
- documenter les erreurs, l'authentification et des exemples ;
- valider le document avec un linter ;
- garder la documentation fidèle au code.
3 exercices corrigés1 schéma
Documenter une API REST, c'est décrire, pour les développeurs des applications qui l'appellent, chaque route : son adresse, ses paramètres, le corps attendu, les réponses possibles avec leurs codes, et la façon de s'authentifier. Le standard pour le faire est OpenAPI, un document YAML ou JSON lisible à la fois par les humains et par les outils. Ce chapitre se concentre sur l'écriture d'une documentation utile : un exemple complet commenté, des descriptions, des exemples et des erreurs bien documentés, la validation avec un linter et le maintien à jour. La structure détaillée d'OpenAPI et ses outils d'affichage sont dans le cours API REST.
Ce qu'un développeur attend d'une documentation d'API
Mets-toi à la place de Chloé, qui doit brancher l'intranet sur l'API du parc. Elle a besoin de savoir :
- à quoi sert l'API et ce qu'elle ne fait pas ;
- comment s'authentifier, avec un exemple ;
- l'adresse de base de chaque environnement ;
- pour chaque route : la méthode, le chemin, les paramètres (obligatoires ou non, type, limites), le corps attendu et les réponses, succès comme erreurs ;
- le format des erreurs, pour les afficher correctement à l'utilisateur ;
- des exemples réalistes, qu'elle peut copier pour tester ;
- ce qui change d'une version à l'autre et ce qui va disparaître.
Une documentation qui ne décrit que les cas où tout se passe bien laisse le développeur deviner le reste, et il devine mal : c'est le 422 non documenté qui finit en « Erreur inconnue » affichée à l'utilisateur.
OpenAPI en 2026 : quelle version ?
| Version | Publication | À retenir |
|---|---|---|
| Swagger 2.0 | 2014 | Ancien nom du format ; encore présent dans de vieux projets (swagger: "2.0") |
| OpenAPI 3.0 | 2017 | Premier nom « OpenAPI » ; components, requestBody, plusieurs serveurs |
| OpenAPI 3.1 | 2021 ; correctif 3.1.2 en septembre 2025 | Les schémas sont du JSON Schema 2020-12 complet ; la version la mieux prise en charge par les outils |
| OpenAPI 3.2 | Septembre 2025 | Compatible avec la 3.1 ; ses nouveautés sont adoptées progressivement par les outils |
Ce chapitre, comme le cours API REST, écrit de l'OpenAPI 3.1 (openapi: 3.1.2). Ne confonds pas ce numéro, qui est celui de la spécification, avec info.version, qui est celui de ton API.
La structure d'un document OpenAPI
Le principe à retenir : paths décrit les opérations, components décrit une seule fois ce qu'elles partagent (les formes de données, les réponses d'erreur, l'authentification), et chaque opération y renvoie par une référence $ref. Quand le schéma d'un poste change, on le modifie à un seul endroit.
L'exemple complet : l'API du parc de Sorbier
L'API du parc expose l'inventaire des postes de la base parc et permet d'ouvrir un ticket. Son document complet fait 255 lignes ; il est téléchargeable ici et passe la validation sans erreur. Voici ses parties, commentées.
L'en-tête : de quoi parle-t-on, où, comment s'authentifier.
openapi: 3.1.2
info:
title: API du parc de Sorbier Logistique
version: 1.2.0
summary: Inventaire des postes et tickets d'assistance du service informatique.
description: |
API interne du service informatique de Sorbier Logistique. Elle expose
l'inventaire des postes de travail et permet d'ouvrir des tickets.
- Les dates suivent la norme ISO 8601 (`2026-11-08`).
- Les erreurs suivent le format de la RFC 9457 (`application/problem+json`).
- L'API n'est joignable que depuis le réseau interne.
contact:
name: Service informatique de Sorbier Logistique
email: informatique@sorbier.example
license:
name: Usage interne, propriété de Sorbier Logistique
identifier: LicenseRef-Sorbier-interne
servers:
- url: https://parc.ad.sorbier.example/api/v1
description: Production, sur le réseau interne
- url: http://localhost:3000/api/v1
description: Poste du développeur
security:
- jeton: []
tags:
- name: Postes
description: Inventaire des postes de travail
- name: Tickets
description: Demandes d'assistance
info.versionvaut1.2.0, comme la dernière version du CHANGELOG (chapitre 5) : la documentation dit de quelle version elle parle.- La
descriptiondonne les conventions communes à toutes les routes (dates, erreurs, accès) une seule fois, au lieu de les répéter partout. Elle accepte le Markdown. license.identifierattend un identifiant de licence au format SPDX (MIT,Apache-2.0…) ; pour une licence maison, SPDX prévoit la formeLicenseRef-….securityau premier niveau exige le jeton pour toutes les opérations ; une route publique l'annulerait avecsecurity: [].
Une opération de lecture.
/postes/{numInventaire}:
get:
tags: [Postes]
summary: Lire un poste
description: Renvoie un poste à partir du numéro collé sur l'unité centrale.
operationId: lirePoste
parameters:
- name: numInventaire
in: path
required: true
description: Numéro d'inventaire du poste.
schema:
$ref: '#/components/schemas/NumInventaire'
example: PC-0008
responses:
'200':
description: Le poste demandé.
content:
application/json:
schema:
$ref: '#/components/schemas/Poste'
'401':
$ref: '#/components/responses/NonAuthentifie'
'404':
$ref: '#/components/responses/Introuvable'
summaryest un titre court qui commence par un verbe ;descriptionajoute ce qu'un développeur doit savoir.- Le paramètre de chemin est déclaré
in: pathetrequired: true: un paramètre de chemin est toujours obligatoire. - Les codes de réponse sont écrits entre apostrophes (
'200') : ce sont des clés de type chaîne. - Les deux erreurs possibles sont documentées, en renvoyant vers des réponses communes.
Un paramètre obsolète. La version 1.2.0 a remplacé le paramètre stock de GET /postes par enStock. L'ancien reste documenté, marqué deprecated, tant qu'il fonctionne :
- name: enStock
in: query
description: "`true` : seulement les postes affectés à personne."
schema:
type: boolean
- name: stock
in: query
deprecated: true
description: Remplacé par `enStock` ; sera supprimé dans la version 2.0.0.
schema:
type: boolean
Les outils d'affichage signalent un paramètre deprecated : le développeur sait qu'il doit migrer, et quand. La même information figure dans la rubrique Obsolète du CHANGELOG.
Une opération d'écriture.
/tickets:
post:
tags: [Tickets]
summary: Ouvrir un ticket
description: |
Ouvre un ticket au nom de l'utilisateur authentifié. Le ticket est créé
avec le statut `ouvert` ; sans priorité, il reçoit la priorité `normale`.
operationId: ouvrirTicket
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/NouveauTicket'
example:
titre: Ventilateur bruyant
priorite: basse
numInventaire: PC-0008
responses:
'201':
description: Ticket créé. L'en-tête `Location` donne son adresse.
headers:
Location:
description: URI du nouveau ticket.
schema:
type: string
example: /api/v1/tickets/15
content:
application/json:
schema:
$ref: '#/components/schemas/Ticket'
'401':
$ref: '#/components/responses/NonAuthentifie'
'422':
$ref: '#/components/responses/Invalide'
- La
descriptiondonne les règles métier que le schéma ne dit pas : le demandeur est l'utilisateur du jeton (pas un champ du corps), le statut de départ, la priorité par défaut. Ce sont les colonnesstatutetprioritede la tableticket, avec leurs valeurs par défaut. - L'
exampledu corps est réaliste : c'est le ticket 6 de la baseparc. Un exemple vraisemblable aide plus qu'unstringrépété. - La réponse
201documente l'en-têteLocationet le corps renvoyé.
Les composants réutilisables (extrait : les champs marque et modele du poste, et les autres réponses d'erreur, sont dans le fichier complet).
components:
securitySchemes:
jeton:
type: http
scheme: bearer
bearerFormat: JWT
description: Jeton délivré par le service d'authentification de l'intranet.
schemas:
NumInventaire:
type: string
pattern: '^PC-[0-9]{4}$'
examples: [PC-0008]
Poste:
type: object
required: [numInventaire, type, marque, modele, dateAchat, prixAchat, affecteA]
properties:
numInventaire:
$ref: '#/components/schemas/NumInventaire'
type:
$ref: '#/components/schemas/TypePoste'
dateAchat:
type: string
format: date
prixAchat:
type: number
minimum: 0
description: Prix d'achat en euros.
affecteA:
type: [string, 'null']
format: email
description: Adresse du salarié qui utilise le poste ; `null` pour un poste en stock.
NouveauTicket:
type: object
required: [titre]
additionalProperties: false
properties:
titre:
type: string
minLength: 5
maxLength: 100
priorite:
$ref: '#/components/schemas/Priorite'
numInventaire:
$ref: '#/components/schemas/NumInventaire'
responses:
Invalide:
description: Le corps de la requête ne respecte pas le schéma.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Probleme'
example:
title: Données invalides
status: 422
detail: Le titre doit contenir au moins 5 caractères.
NumInventaireest défini une fois avec son motif (PC-et quatre chiffres) et réutilisé par le paramètre de chemin, le poste et le nouveau ticket.type: [string, 'null']est la syntaxe d'OpenAPI 3.1 pour un champ qui peut valoirnull(l'anciennullable: truede la 3.0 n'existe plus). Ici,nulla un sens métier, expliqué dans la description : le poste est en stock.- Les unités sont dites (
Prix d'achat en euros) : un nombre sans unité est une source d'erreur. additionalProperties: falserefuse tout champ inattendu dans le corps : un client ne peut pas glisser unstatut: resoluà la création.- Chaque réponse d'erreur a un exemple au format de la RFC 9457 : le développeur voit exactement ce qu'il recevra et peut afficher
detailà l'utilisateur.
Écrire pour le développeur
| À faire | Pourquoi |
|---|---|
Un summary court qui commence par un verbe | Il sert de titre dans la documentation affichée |
Une description pour chaque paramètre, avec l'unité et les limites | limite ou duree ne disent rien sans unité ni maximum |
Les règles métier dans la description de l'opération | Qui a le droit, ce qui est créé, les valeurs par défaut |
| Toutes les réponses d'erreur prévues, avec un exemple | Le client sait quoi afficher, au lieu de « Erreur inconnue » |
| Des exemples tirés du jeu d'essai | Réalistes et cohérents entre eux |
deprecated: true sur ce qui va disparaître | Le développeur migre avant la rupture |
info.version alignée sur le CHANGELOG | On sait de quelle version parle la documentation |
Valider le document avec un linter
Un document OpenAPI est du code : une indentation décalée, une référence mal orthographiée, et les outils le refusent ou, pire, l'affichent à moitié. Un linter le vérifie. Redocly CLI fonctionne sans installation préalable, avec npx :
npx @redocly/cli lint openapi.yaml
Sur le document de l'API du parc, il répond (extrait : l'outil affiche aussi les lignes concernées et un lien vers la règle) :
validating openapi.yaml...
[1] openapi.yaml:22:10 at #/servers/1/url
Server `url` should not point to example.com or localhost.
Warning was generated by the no-server-example.com rule.
Woohoo! Your API description is valid. 🎉
You have 1 warning.
Le document est valide ; l'avertissement vient d'une règle de style du jeu de règles recommandé, qui déconseille un serveur localhost dans une documentation publiée. Pour une documentation interne destinée aussi aux développeurs, ce serveur est voulu : on désactive la règle dans un fichier redocly.yaml placé à côté du document.
extends:
- recommended
rules:
no-server-example.com: off
Une vraie erreur bloque la validation. Si l'on se trompe dans le nom d'un composant (NouveauTickett), Redocly CLI signale Can't resolve $ref avec la ligne, et la validation échoue (Validation failed). Ajoute la commande de lint à ton pipeline d'intégration continue : une documentation cassée bloque la fusion (le cours Intégration continue).
D'autres outils font le même travail : Spectral (npx @stoplight/spectral-cli lint openapi.yaml, avec un fichier de règles .spectral.yaml), l'extension OpenAPI de VS Code, ou Swagger Editor en ligne, qui valide pendant la frappe.
Produire une page de documentation. Redocly CLI génère aussi une page HTML autonome, lisible par n'importe qui, à partir du même document :
npx @redocly/cli build-docs openapi.yaml -o docs/api.html
Swagger UI, Redoc et Scalar, qui affichent une documentation interactive, sont comparés dans le cours API REST ; le cours Node.js montre comment servir Swagger UI depuis une API Express.
Garder la documentation fidèle au code
Une documentation d'API devient fausse dès qu'une route change sans elle. Trois habitudes l'en empêchent :
- La même pull request modifie la route et son document OpenAPI ; le relecteur refuse l'une sans l'autre.
- Le choix entre la description d'abord et le code d'abord : écrire le document avant le code pour une API utilisée par d'autres équipes, le générer à partir du code (annotations, schémas de validation) pour une petite API interne. Le cours API REST détaille les deux approches.
- Des tests de contrat qui vérifient que les réponses réelles de l'API respectent les schémas du document : si l'API renvoie un champ absent de la documentation, le test échoue (tester une API).
Exercices
Exercice 1 sur 3 Lire la documentation Réussi À revoir
À partir des extraits de ce chapitre, réponds.
- Quelle est l'URL complète pour lire le poste PC-0008 en production ?
- Chloé envoie
POST /ticketsavec le corps{"titre": "Bruit"}. Que se passe-t-il, et que reçoit-elle ? - Elle envoie
{"titre": "Écran noir au démarrage"}, sans priorité. Quelle priorité aura le ticket, et où est-ce écrit ? - Un ancien script appelle
GET /postes?stock=true. Fonctionne-t-il encore ? Que doit faire son auteur ?
Voir la réponseMasquer la réponse de l'exercice 1
https://parc.ad.sorbier.example/api/v1/postes/PC-0008: l'adresse du serveur de production suivie du chemin, avec{numInventaire}remplacé.- Le titre fait 5 caractères (« Bruit ») : il respecte
minLength: 5. Le ticket est créé : réponse201, avec l'en-têteLocationet le ticket en JSON. Un titre de 4 caractères aurait donné une422au formatapplication/problem+json. - La priorité
normale: c'est écrit dans ladescriptionde l'opération (« sans priorité, il reçoit la prioriténormale»), pas dans le schéma. C'est exactement le genre de règle métier qu'une bonne description doit porter. - Oui, pour l'instant : le paramètre
stockestdeprecated, pas supprimé. Il sera supprimé dans la version 2.0.0 : l'auteur du script doit remplacerstockparenStockdès maintenant.
Tu avais trouvé ?
Exercice 2 sur 3 Lire un rapport de linter Réussi À revoir
Un collègue a modifié le document et Redocly CLI affiche deux erreurs :
[1] openapi.yaml:80:7 at #/paths/~1postes~1{numInventaire}/get/parameters
The operation does not define the path parameter `{numInventaire}` expected by path `/postes/{numInventaire}`.
[2] openapi.yaml:111:15 at #/paths/~1tickets/post/requestBody/content/application~1json/schema
Can't resolve $ref
Explique chaque erreur et sa correction probable. Que signifie ~1 dans les chemins affichés ?
Voir la réponseMasquer la réponse de l'exercice 2
- Le chemin
/postes/{numInventaire}contient un paramètre, mais l'opération ne déclare aucun paramètrein: pathde ce nom : le collègue a probablement écritin: query, ou supprimérequired: trueen même temps. Correction :in: pathetrequired: true. - La référence du corps de
POST /ticketspointe vers un composant qui n'existe pas : une faute de frappe dans le nom (par exempleNouveauTickett). Correction : écrire exactement'#/components/schemas/NouveauTicket'.
~1 est l'encodage de la barre oblique / dans un pointeur JSON (RFC 6901) : #/paths/~1postes~1{numInventaire} désigne la clé /postes/{numInventaire} de paths, dont le nom contient des barres obliques.
Tu avais trouvé ?
Exercice 3 sur 3 Documenter une nouvelle route Réussi À revoir
Ajoute l'opération GET /tickets/{id} : elle renvoie un ticket (200, schéma Ticket). Un salarié ne lit que ses propres tickets ; un technicien les lit tous. Prévois les réponses 401 et 404. Attention : la réponse commune Introuvable parle d'un poste.
Voir la réponseMasquer la réponse de l'exercice 3
/tickets/{id}:
get:
tags: [Tickets]
summary: Lire un ticket
description: |
Renvoie un ticket. Un salarié ne lit que ses propres tickets ;
un technicien du service informatique les lit tous.
operationId: lireTicket
parameters:
- name: id
in: path
required: true
description: Numéro du ticket.
schema:
type: integer
minimum: 1
example: 6
responses:
'200':
description: Le ticket demandé.
content:
application/json:
schema:
$ref: '#/components/schemas/Ticket'
'401':
$ref: '#/components/responses/NonAuthentifie'
'404':
description: Ticket inexistant, ou appartenant à un autre salarié.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Probleme'
La 404 est écrite sur place, avec une description juste : réutiliser Introuvable, qui annonce « Aucun poste ne porte ce numéro d'inventaire », mettrait une phrase fausse dans la documentation. Remarque aussi le choix de répondre 404, et non 403, quand un salarié demande le ticket d'un autre : l'API ne révèle pas l'existence d'un ticket qu'il n'a pas le droit de voir. Avec cette opération, le document passe toujours la validation de Redocly CLI.
Tu avais trouvé ?