Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 623 minIntermédiaire

Concevoir une API REST : pagination, filtres, versionnement, erreurs

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • rédiger le tableau des opérations d'une API à partir d'un besoin ;
  • paginer une collection par décalage ou par curseur ;
  • versionner une API sans casser ses clients ;
  • renvoyer des erreurs au format de la RFC 9457 ;
  • protéger les créations et les modifications concurrentes.

5 exercices corrigés1 schéma

Concevoir une API REST, c'est fixer son contrat avant d'écrire le code : les ressources et leurs URI, les opérations permises à chaque rôle, la forme des requêtes et des réponses, les erreurs, et les règles communes comme la pagination ou le versionnement. Ce chapitre donne une méthode en six étapes appliquée à AppliFrais, puis les bonnes pratiques que tout développeur d'API doit connaître : pagination, filtres et tri, versionnement, erreurs au format de la RFC 9457, clé d'idempotence, verrouillage optimiste et traitements longs.

Une méthode en six étapes

  1. Lister les acteurs et leurs besoins. Qui appelle l'API, pour faire quoi ? Le front des visiteurs, l'écran des comptables, l'application mobile, un partenaire ?
  2. En déduire les ressources. Les noms qui reviennent dans les besoins : fiche, frais, visiteur, justificatif, validation.
  3. Écrire le tableau des opérations : méthode, URI, rôle autorisé, code de succès. C'est le cœur du contrat.
  4. Définir les représentations : ce que le client envoie et ce qu'il reçoit, champ par champ, avec les règles de validation.
  5. Lister les erreurs de chaque opération : quelles règles de gestion peuvent échouer, avec quel code.
  6. Fixer les règles transverses : authentification, pagination, format des dates, versionnement, limites de débit.

Le résultat s'écrit dans un document OpenAPI (chapitre 7) : il sert de référence au développeur du front, au développeur de l'API et au testeur, qui peuvent travailler en parallèle.

Le tableau des opérations d'AppliFrais

Les règles de gestion : un visiteur ne voit et ne modifie que ses propres fiches ; il saisit ses frais tant que la fiche est en cours (CR), puis la clôture (CL) ; un comptable consulte toutes les fiches clôturées, les valide (VA) en fixant le montant validé, puis les met en paiement (MP).

OpérationMéthode et URIRôleSuccèsErreurs métier
Lister mes fichesGET /v1/fiches?mois=&etat=visiteur200—
Lister les fiches à validerGET /v1/fiches?etat=CL&visiteur=comptable200—
Créer la fiche du moisPOST /v1/fichesvisiteur201409 si elle existe déjà ; 422 si le mois est invalide
Lire une ficheGET /v1/fiches/{id}propriétaire ou comptable200404 si inexistante ou fiche d'un autre visiteur
Saisir les frais forfaitisésPUT /v1/fiches/{id}/lignes-forfaitpropriétaire200409 si la fiche n'est plus en cours ; 422 si une quantité est négative
Ajouter un frais hors forfaitPOST /v1/fiches/{id}/frais-hors-forfaitpropriétaire201409 si la fiche n'est plus en cours ; 422
Supprimer un frais hors forfaitDELETE /v1/frais-hors-forfait/{id}propriétaire204409 si la fiche n'est plus en cours
Clôturer une fichePATCH /v1/fiches/{id} avec {"etat":"CL"}propriétaire200409 si la fiche n'est pas à l'état CR
Valider une fichePOST /v1/fiches/{id}/validationscomptable201409 si la fiche n'est pas à l'état CL ; 422 si le montant est incohérent
Mettre en paiementPATCH /v1/fiches/{id} avec {"etat":"MP"}comptable200409 si la fiche n'est pas à l'état VA

Pour toutes les opérations, les erreurs communes s'ajoutent : 401 sans jeton valide, 403 si le rôle ne permet pas l'opération, 400 si la requête est illisible, 429 au-delà de la limite de débit.

Ce que l'on envoie, ce que l'on reçoit

La représentation reçue par le client n'est pas la ligne de la base :

  • elle ne contient jamais de champ interne ou sensible (hash de mot de passe, notes internes, identifiants techniques sans intérêt pour le client). Sélectionne explicitement les champs, ou passe par un objet de réponse dédié (un DTO, Data Transfer Object) ;
  • dans une liste, on renvoie un résumé de chaque élément (identifiant, mois, état, montant) ; le détail (lignes de frais, justificatifs) s'obtient avec GET sur l'élément. Une liste de 50 fiches avec toutes leurs lignes serait lourde et lente ;
  • les champs calculés utiles au client (le total d'une fiche) peuvent figurer dans la représentation, mais le client ne les envoie jamais : le serveur les recalcule.

La représentation envoyée par le client est plus restreinte encore. Pour créer un frais hors forfait, le client envoie la date, le libellé et le montant ; il n'envoie ni l'id (généré par la base), ni la fiche de rattachement (elle est dans l'URI), ni l'état de remboursement (décidé par le comptable). Tout champ inattendu est refusé ou ignoré : c'est la liste blanche vue avec JSON Schema.

La pagination

Une collection peut contenir des milliers d'éléments : on ne la renvoie jamais en entier. La pagination découpe la liste en pages. Deux techniques dominent.

La pagination par décalage (offset). Le client demande une page et une taille ; le serveur saute les premiers éléments.

http
GET /v1/fiches?etat=CL&page=3&limite=20 HTTP/1.1
json
{
  "donnees": [ "… 7 fiches …" ],
  "pagination": { "page": 3, "limite": 20, "total": 47, "pages": 3 }
}

Avec 47 fiches et 20 par page, il y a 3 pages (47 ÷ 20 = 2,35, arrondi au-dessus) ; la page 3 saute (3 − 1) × 20 = 40 fiches et contient les 7 dernières. Côté SQL, c'est un LIMIT 20 OFFSET 40 (pagination en SQL). Simple, et le client peut sauter directement à une page. Deux défauts : sur une grande table, OFFSET 100000 oblige la base à lire puis jeter cent mille lignes ; et si une fiche est ajoutée pendant que le client parcourt les pages, un élément est vu deux fois ou sauté.

La pagination par curseur (cursor ou keyset). Le serveur renvoie, avec chaque page, un curseur opaque qui désigne le dernier élément lu ; le client le renvoie pour obtenir la suite.

http
GET /v1/fiches?etat=CL&limite=20&apres=eyJpZCI6MTE4MH0 HTTP/1.1
json
{
  "donnees": [ "… 20 fiches …" ],
  "pagination": { "suivant": "eyJpZCI6MTIwMH0", "limite": 20 }
}

Le curseur eyJpZCI6MTE4MH0 est simplement {"id":1180} encodé en base64url : le serveur fait WHERE id > 1180 ORDER BY id LIMIT 20, qui utilise l'index de la clé primaire quelle que soit la profondeur. Pas de doublon ni de trou si des fiches sont ajoutées ; en revanche, impossible de sauter à la page 37. C'est la technique des fils d'actualité et des API à gros volumes.

Par décalageGET /v1/fiches?page=3&limite=20fiches 1 à 40lues puis jetées par la base (OFFSET 40)fiches 41 à 47renvoyéesréponse :"pagination": {"page":3,"limite":20,"total":47,"pages":3}Par curseurGET /v1/fiches?apres=eyJpZCI6MTE4MH0&limite=20le curseur décodé vaut {"id":1180}id ≤ 1180jamais lues20 fiches, id > 1180renvoyées ; la dernière : 1200la suite…l'index sur id saute directement iciréponse :"pagination": {"suivant":"eyJpZCI6MTIwMH0","limite":20}
Décalage : on saute des lignes ; curseur : on repart du dernier élément vu
Par décalage (page, limite)Par curseur (apres, limite)
Aller à une page préciseOuiNon
Performance sur une grande tableSe dégrade avec la profondeurConstante (index)
Ajouts pendant le parcoursDoublons ou éléments sautésAucun problème
Nombre total d'élémentsFacile (COUNT)Rarement fourni
Usage typiqueÉcrans d'administration, tableaux paginésDéfilement infini, synchronisation, exports

Dans tous les cas, le serveur plafonne la taille de page : limite vaut 20 par défaut et 100 au maximum, quelle que soit la valeur demandée. Sans plafond, ?limite=1000000 vide la table en une requête et sature le serveur (API4 de l'OWASP, chapitre 10).

Filtrer, trier, choisir les champs

Ces options passent toutes par la chaîne de requête. Il n'existe pas de norme unique ; voici des conventions répandues, à appliquer de façon cohérente dans toute l'API.

BesoinConventionExemple
Filtre d'égalitéLe nom du champ?etat=CL&mois=2026-10
Plusieurs valeursValeurs séparées par des virgules?etat=CL,VA
IntervalleSuffixes Min et Max?montantMin=100&montantMax=500
Recherche plein texteq?q=lyon
Tritri, un - pour l'ordre décroissant?tri=-mois,visiteur
Champs renvoyéschamps?champs=id,mois,etat

Deux règles de sécurité : n'accepte comme critère de tri ou de filtre que des champs prévus (une liste blanche), et ne construis jamais la requête SQL en y collant le paramètre. Un ORDER BY ne se paramètre pas avec une requête préparée : on vérifie que le nom du champ figure dans la liste autorisée avant de l'utiliser (requêtes préparées).

Versionner une API

Une API publiée a des clients que tu ne contrôles pas : des applications mobiles installées sur des téléphones, le logiciel d'un partenaire. Un changement incompatible (breaking change) les casse du jour au lendemain. Le versionnement permet de faire évoluer l'API sans casser les clients existants.

ChangementCompatible ?
Ajouter une nouvelle routeOui
Ajouter un champ dans une réponseOui (si les clients ignorent les champs inconnus, ce qu'ils doivent faire)
Ajouter un paramètre facultatifOui
Renommer ou supprimer un champ d'une réponseNon
Changer le type d'un champ ("id": 12 devient "id": "12")Non
Rendre obligatoire un champ qui était facultatifNon
Supprimer une route, ou changer le sens d'un code de statutNon
Ajouter une valeur possible à une énumération (etat peut valoir RB)À risque : un client qui traite tous les cas plante sur la valeur inconnue

Trois façons d'indiquer la version :

StratégieExempleAvantagesInconvénients
Dans l'URI/v1/fiches, /v2/fichesVisible, simple à tester et à routerL'URI d'une même ressource change entre versions
Dans un en-têteAccept: application/vnd.gsb.v2+json ou un en-tête dédiéURI stablesInvisible dans le navigateur, plus difficile à tester
Dans la chaîne de requête/fiches?version=2SimpleSe mélange avec les filtres ; mauvais pour le cache

La version dans l'URI, avec seulement le numéro majeur (v1, v2), est la plus répandue et la plus simple : c'est celle d'AppliFrais. On ne change de version majeure que pour un changement incompatible ; tous les changements compatibles s'ajoutent dans la version courante.

Quand une version doit disparaître, on prévient les clients avant de l'arrêter. Deux en-têtes normalisés le permettent :

http
HTTP/1.1 200 OK
Deprecation: @1798761600
Sunset: Tue, 01 Jun 2027 00:00:00 GMT
Link: <https://api.gsb.example/docs/migration-v2>; rel="deprecation"

Deprecation (RFC 9745, 2025) donne la date à partir de laquelle la ressource est dépréciée (ici un horodatage Unix, le 1er janvier 2027 ; une date future annonce une dépréciation à venir) ; Sunset (RFC 8594) donne la date à laquelle elle cessera de répondre ; le lien pointe vers le guide de migration. Après la date de fin, la route répond 410 Gone. L'erreur classique, sanctionnée par l'OWASP (API9, gestion d'inventaire défaillante), est de laisser tourner une vieille version oubliée, qui n'a pas reçu les correctifs de sécurité de la nouvelle.

Des erreurs normalisées : la RFC 9457

Le code de statut dit quel type d'erreur ; le corps doit dire précisément quoi, pour le développeur du client et parfois pour l'utilisateur. Plutôt que d'inventer un format par API, la RFC 9457 (Problem Details for HTTP APIs, juillet 2023, qui remplace la RFC 7807) définit un format standard, avec le type de contenu application/problem+json. Voici la réponse réelle de l'API de démonstration à une clôture impossible :

http
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.gsb.example/problemes/transition-interdite",
  "title": "Changement d'état interdit",
  "status": 409,
  "detail": "Une fiche à l'état CR ne peut pas passer à l'état VA.",
  "instance": "/v1/fiches/12",
  "etatActuel": "CR"
}
MembreRôle
typeUne URI qui identifie le type de problème ; c'est ce que le code client teste. Elle peut pointer vers une page de documentation. Par défaut : about:blank
titleUn résumé lisible du type de problème, le même pour toutes ses occurrences
statusLe code de statut HTTP, répété dans le corps
detailUne explication propre à cette occurrence
instanceUne URI qui identifie cette occurrence (souvent la ressource concernée)
extensionsTout membre supplémentaire utile : ici etatActuel

Pour une erreur de validation, une extension liste les champs en faute, ce qui permet au front d'afficher chaque message à côté du bon champ :

http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://api.gsb.example/problemes/validation",
  "title": "Données invalides",
  "status": 422,
  "detail": "Un ou plusieurs champs sont invalides.",
  "erreurs": [
    { "champ": "mois", "message": "Format attendu : AAAA-MM" }
  ]
}

Rendre un POST rejouable : la clé d'idempotence

Le visiteur valide l'ajout d'un frais depuis son téléphone dans un train ; la requête part, le réseau coupe avant la réponse. L'application ne sait pas si le frais a été créé. Si elle renvoie le POST, elle risque un doublon ; si elle ne le renvoie pas, le frais est peut-être perdu.

La solution : le client génère un identifiant unique (un UUID) pour chaque opération et l'envoie dans l'en-tête Idempotency-Key. Il réutilise la même clé s'il renvoie la même opération.

http
POST /v1/fiches/12/frais-hors-forfait HTTP/1.1
Content-Type: application/json
Idempotency-Key: aed7abd4-0a0d-4845-8989-a1cd1792344e

{"date":"2026-10-14","libelle":"Repas client à Lyon","montant":23.50}

Le serveur enregistre la clé avec la réponse qu'il a produite. S'il reçoit à nouveau la même clé, il ne refait rien et renvoie la réponse enregistrée. Ce mécanisme a été popularisé par les API de paiement (un double débit est inacceptable) ; l'en-tête Idempotency-Key est en cours de normalisation à l'IETF. Les clés sont conservées un temps limité (24 heures, par exemple).

Éviter les mises à jour perdues : ETag et If-Match

Deux comptables ouvrent la même fiche à 10 h 00. La première corrige le montant validé et enregistre à 10 h 02 ; le second, qui a toujours l'ancienne version à l'écran, enregistre à 10 h 03 et écrase sa correction sans le savoir. C'est la mise à jour perdue.

Le verrouillage optimiste l'empêche avec l'ETag vu au chapitre 2 :

  1. Chaque GET renvoie l'ETag de la version lue.
  2. Chaque modification renvoie cet ETag dans l'en-tête If-Match.
  3. Le serveur compare : si la ressource a changé entre-temps, son ETag est différent, et il refuse avec 412 Precondition Failed.
http
PATCH /v1/fiches/12 HTTP/1.1
If-Match: "1434c386cc94c088"
Content-Type: application/json

{"etat":"CL"}

Le second comptable reçoit 412, recharge la fiche, voit la correction de sa collègue et décide en connaissance de cause. Une API peut même exiger If-Match sur toutes les modifications, et répondre 428 Precondition Required s'il manque. Côté base, l'ETag peut venir d'une colonne version incrémentée à chaque modification, ou d'une empreinte de la ligne.

Les traitements longs : 202 Accepted

Générer l'export PDF de toutes les fiches de l'année prend une minute : on ne fait pas attendre la connexion HTTP aussi longtemps. L'API accepte la demande et répond immédiatement :

http
POST /v1/exports HTTP/1.1
Content-Type: application/json

{"format":"pdf","annee":2026}
http
HTTP/1.1 202 Accepted
Location: /v1/exports/9f1e

{"id":"9f1e","statut":"en-cours"}

Le client interroge GET /v1/exports/9f1e de temps en temps : "statut": "en-cours", puis "statut": "termine" avec l'adresse du fichier. Ou bien l'API le prévient par un webhook quand c'est prêt (chapitre 12).

La liste de contrôle du concepteur

Avant de coder, relis ton contrat avec ces questions :

  • Les URI sont des noms au pluriel, en minuscules avec des tirets, sans verbe ni extension.
  • Chaque opération a sa méthode, son code de succès, ses erreurs, et le rôle autorisé.
  • Les méthodes sûres (GET) ne modifient rien.
  • Les réponses ne contiennent aucun champ sensible ou interne ; les entrées sont validées par liste blanche.
  • Les collections sont paginées, avec une taille de page plafonnée.
  • Les dates sont en ISO 8601, les montants dans un type exact, les identifiants longs en chaînes.
  • Les erreurs suivent le format application/problem+json, sans détail technique.
  • La version est dans l'URI ; un plan existe pour retirer les anciennes versions.
  • Les opérations sensibles au doublon acceptent une clé d'idempotence ; les modifications concurrentes sont protégées par If-Match.
  • Tout passe en HTTPS, chaque requête est authentifiée, chaque accès à un objet vérifie le propriétaire.

Exercices

Exercice 1 sur 5 Concevoir les routes d'un cas Réussi À revoir

Une association sportive veut une API pour gérer les inscriptions à ses séances. Un adhérent consulte le planning des séances de la semaine, s'inscrit à une séance s'il reste des places, et annule son inscription jusqu'à deux heures avant le début. Un coach crée des séances (date, heure, salle, nombre de places), consulte la liste des inscrits de ses séances et marque les présences.

Rédige le tableau des opérations : méthode et URI, rôle, code de succès et erreurs métier.

Voir la réponseMasquer la réponse de l'exercice 1

Une proposition (d'autres choix sont défendables s'ils sont cohérents) :

OpérationMéthode et URIRôleSuccèsErreurs métier
Planning de la semaineGET /v1/seances?du=2026-11-02&au=2026-11-08adhérent, coach200400 si les dates sont mal formées
Détail d'une séance (avec places restantes)GET /v1/seances/{id}adhérent, coach200404
Créer une séancePOST /v1/seancescoach201 + Location422 si la date est passée ou le nombre de places ≤ 0 ; 409 si la salle est déjà prise
S'inscrirePOST /v1/seances/{id}/inscriptionsadhérent201409 si la séance est complète ou si l'adhérent est déjà inscrit
Annuler son inscriptionDELETE /v1/inscriptions/{id}adhérent propriétaire204409 à moins de deux heures du début ; 404 si l'inscription est celle d'un autre
Liste des inscritsGET /v1/seances/{id}/inscriptionscoach de la séance200403 si ce n'est pas sa séance
Marquer une présencePATCH /v1/inscriptions/{id} avec {"present":true}coach de la séance200403 ; 409 si la séance n'a pas encore commencé

Pour toutes les routes : 401 sans jeton, 403 si le rôle ne convient pas, 429 au-delà de la limite. L'adhérent est identifié par son jeton : il n'envoie jamais son propre identifiant dans le corps.

Tu avais trouvé ?

Exercice 2 sur 5 Calculer une pagination Réussi À revoir

La collection /v1/fiches?etat=VA contient 1 250 fiches. Le client demande limite=50.

  1. Combien y a-t-il de pages ?
  2. Combien de fiches la base saute-t-elle pour la page 13 ? Écris la clause SQL correspondante.
  3. Une fiche validée de plus est ajoutée. Combien de pages maintenant, et combien de fiches sur la dernière ?
  4. Le client envoie limite=5000. Que doit faire le serveur ?
Voir la réponseMasquer la réponse de l'exercice 2
  1. 1 250 ÷ 50 = 25 pages exactement.
  2. (13 − 1) × 50 = 600 fiches sautées : LIMIT 50 OFFSET 600.
  3. 1 251 ÷ 50 = 25,02, arrondi au-dessus : 26 pages ; la dernière contient 1 251 − 25 × 50 = 1 fiche.
  4. Plafonner : utiliser la limite maximale (100, par exemple) et l'indiquer dans la réponse ("limite": 100), ou refuser avec 400 en expliquant le maximum. Il ne doit jamais renvoyer 5 000 fiches.

Tu avais trouvé ?

Exercice 3 sur 5 Rédiger une erreur RFC 9457 Réussi À revoir

Un visiteur tente d'ajouter un frais hors forfait de 23,50 € à la fiche 11, qui est validée. Écris la réponse complète de l'API (ligne de statut, en-tête Content-Type, corps) au format de la RFC 9457.

Voir la réponseMasquer la réponse de l'exercice 3
http
HTTP/1.1 409 Conflict
Content-Type: application/problem+json

{
  "type": "https://api.gsb.example/problemes/fiche-non-modifiable",
  "title": "Fiche non modifiable",
  "status": 409,
  "detail": "La fiche 11 est à l'état VA : on ne peut plus y ajouter de frais.",
  "instance": "/v1/fiches/11",
  "etatActuel": "VA"
}

Le code est 409 : la requête est bien formée et les données valides, mais l'état de la fiche interdit l'opération. Le type identifie le problème de façon stable (le client peut le tester), le detail explique cette occurrence. Si la fiche 11 appartenait à un autre visiteur, la réponse serait 404, sans rien révéler de son état.

Tu avais trouvé ?

Exercice 4 sur 5 Changement compatible ou incompatible ? Réussi À revoir

L'équipe prépare une évolution de l'API. Pour chaque changement, dis s'il est compatible avec les clients existants, et s'il impose une version v2.

  1. Ajouter le champ total dans la réponse de GET /v1/fiches/{id}.
  2. Renommer montantValide en montantRembourse.
  3. Ajouter un filtre facultatif ?region= sur GET /v1/fiches.
  4. Rendre obligatoire le champ justificatif à la création d'un frais hors forfait.
  5. Passer les identifiants de nombres à chaînes.
  6. Ajouter la route GET /v1/statistiques.
Voir la réponseMasquer la réponse de l'exercice 4
  1. Compatible : un client bien écrit ignore les champs qu'il ne connaît pas.
  2. Incompatible : les clients qui lisent montantValide ne trouvent plus rien. v2, ou bien renvoyer les deux champs pendant une période de transition et déprécier l'ancien.
  3. Compatible : les anciens appels, sans le filtre, fonctionnent comme avant.
  4. Incompatible : les clients qui n'envoient pas de justificatif reçoivent désormais 422. v2, ou une règle appliquée seulement aux nouveaux clients.
  5. Incompatible : un client qui compare fiche.id === 12 ou fait des calculs sur l'identifiant casse. v2.
  6. Compatible : une nouvelle route ne gêne personne.

Tu avais trouvé ?

Exercice 5 sur 5 Offset ou curseur ? Réussi À revoir

Pour chaque écran, choisis la pagination par décalage ou par curseur, et justifie.

  1. L'écran du comptable affiche les fiches clôturées dans un tableau, avec des boutons « page 1, 2, 3… » et le nombre total.
  2. L'application mobile synchronise chaque nuit toutes les fiches modifiées depuis la veille, plusieurs dizaines de milliers de lignes.
  3. Le fil des notifications d'un visiteur, en défilement infini.
Voir la réponseMasquer la réponse de l'exercice 5
  1. Décalage : il faut sauter à une page précise et afficher le total ; les volumes d'un écran de comptable restent raisonnables.
  2. Curseur : gros volume, parcours complet, et des fiches peuvent être modifiées pendant la synchronisation. Le curseur évite les doublons et les trous, et garde des performances constantes.
  3. Curseur : on ne fait que « charger la suite », et de nouvelles notifications arrivent en permanence en tête de liste ; avec un décalage, elles décaleraient les pages et provoqueraient des doublons.

Tu avais trouvé ?

Chapitre terminé !

Fais les 5 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Documenter une API avec OpenAPI et SwaggerRevoir le sommaire du cours