Skip to content
jsonforge.app
Retour au blog
Guide9 min de lecture

Conception des réponses JSON d'API REST : bonnes pratiques et anti-patterns

Les octets renvoyés par une API REST comptent autant que les routes et les codes de statut. Chaque client qui appelle votre API est écrit contre la forme de son JSON, et remodeler ce JSON plus tard casse ces clients, même si le changement paraît minime en interne. Ce guide couvre les décisions de conception de réponse qui coûtent peu au départ et cher une fois que des clients existent.

Choisissez une forme d'enveloppe et ne vous en écartez jamais

Une enveloppe est l'enveloppe de haut niveau cohérente que chaque réponse utilise, quel que soit le point de terminaison : un ensemble connu de clés comme `data`, `error` et `meta` que la couche HTTP d'un client peut traiter génériquement au lieu d'écrire une analyse spécifique par point de terminaison. Sans enveloppe, chaque point de terminaison qui renvoie une forme légèrement différente oblige chaque client à écrire un analyseur légèrement différent.

Une enveloppe cohérente : succès et échec partagent la même forme de haut niveau.
json
// Success
{
  "data": { "id": 42, "name": "Ada Lovelace" },
  "meta": { "requestId": "a1b2c3" }
}

// Failure
{
  "data": null,
  "error": { "code": "NOT_FOUND", "message": "User 42 does not exist" },
  "meta": { "requestId": "a1b2c4" }
}

Les noms de clés précis importent moins que la cohérence : quoi que vous choisissiez, chaque point de terminaison le renvoie, `data` est toujours là où vit la charge utile, et un client peut écrire une seule fonction `unwrap(response)` pour toute l'API au lieu d'une par ressource.

Ne renvoyez jamais un tableau (ou un scalaire) nu au niveau supérieur

Un `GET /users` qui renvoie un tableau JSON nu (`[{...}, {...}]`) paraît naturel, mais vous ne pourrez plus ajouter de métadonnées au niveau réponse sans casser la compatibilité. Dès que vous aurez besoin d'un total, d'un curseur de page suivante ou d'un avertissement à côté de la liste, il faudra passer le type de haut niveau de tableau à objet, ce qui casse tous les clients qui font `response.map(...)` ou `response.length` sur le corps analysé.

Enveloppez les collections dans un champ nommé dès le premier jour (`{ "users": [...] }` ou `{ "data": [...] }`), même si vous êtes sûr de ne jamais avoir besoin de métadonnées supplémentaires. Ajouter une clé sœur à un objet reste rétrocompatible. Transformer un tableau en objet casse tous les clients, dans tous les langages.

Null contre omis : ce ne sont pas les mêmes signaux

Un champ présent avec la valeur `null` et un champ totalement absent de la réponse se ressemblent à première vue (« pas de valeur »), mais ils signifient des choses différentes et les clients doivent pouvoir les distinguer. `null` dit : ce champ est un concept réel et connu pour cette ressource, et sa valeur actuelle est explicitement rien (un utilisateur sans `middleName`). Omis dit : ce champ ne s'applique pas, n'a pas été chargé ou n'a pas été demandé (une requête à champs épars n'ayant demandé que `id` et `name`).

La sémantique PATCH repose sur cette distinction. Avec JSON Merge Patch (RFC 7396), envoyer un champ à `null` le supprime de la cible, tandis qu'omettre le champ dans le corps du patch le laisse inchangé. Si vous confondez les deux, ou restez flou sur les champs qui peuvent être `null`, les clients ne peuvent pas distinguer « effacer cette valeur » de « je n'ai pas envoyé de mise à jour pour ce champ ».

Pagination : curseur contre décalage, et leur forme en JSON

La pagination par décalage/limite (`?offset=40&limit=20`, ou `?page=3&pageSize=20`) est la plus simple à implémenter et à raisonner, mais elle a un vrai problème de correction : si des lignes sont insérées ou supprimées entre les requêtes de pages, les décalages bougent sous les pieds du client, provoquant des lignes sautées ou dupliquées. Elle devient aussi coûteuse en SQL à grande profondeur, puisque la base doit quand même parcourir et écarter toutes les lignes sautées.

La pagination par curseur remet au client un jeton opaque pointant vers « la ligne après la dernière vue » plutôt qu'une position numérique, donc les résultats restent stables quand les données changent et les requêtes restent bon marché à n'importe quelle profondeur. En contrepartie, les clients ne peuvent pas sauter à un numéro de page arbitraire. Ils avancent (et, si vous le prenez en charge, reculent) depuis un curseur.

Une réponse paginée par curseur : aucun calcul de décalage, seulement un jeton opaque de page suivante.
json
{
  "data": [ { "id": 101 }, { "id": 102 } ],
  "meta": {
    "nextCursor": "eyJpZCI6MTAyfQ==",
    "hasMore": true
  }
}

Choisissez par défaut la pagination par curseur pour les données qui changent souvent (flux, journaux, flux d'événements) ou les grandes tables. La pagination par décalage convient aux petites collections plutôt statiques où sauter à la page N rend service (un tableau d'administration avec sélecteur de page).

Objets d'erreur et conventions de nommage

Une réponse d'erreur devrait porter plus qu'un code de statut HTTP et une chaîne : un `code` stable et lisible par une machine sur lequel un client peut brancher sans risque (`"VALIDATION_ERROR"`, pas une phrase), un `message` lisible par l'humain destiné aux journaux et aux développeurs plutôt qu'à l'analyse, et pour les échecs de validation, un tableau `details` nommant exactement quels champs ont échoué et pourquoi.

Une forme d'erreur sur laquelle un client peut agir.
json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request failed validation",
    "details": [
      { "field": "email", "message": "must be a valid email address" },
      { "field": "age", "message": "must be at least 0" }
    ]
  }
}

Pour nommer les clés, camelCase et snake_case conviennent tous les deux. Le mal vient du mélange entre points de terminaison d'une même API, car chaque client a alors besoin d'une logique de correspondance différente selon le point de terminaison. Les consommateurs JavaScript/TypeScript préfèrent souvent camelCase, qui correspond à la déstructuration native ; les écosystèmes Python et Ruby préfèrent souvent snake_case. Choisissez-en un pour toute l'API et appliquez-le aussi aux booléens (`isActive`/`hasAccess`, pas un mélange de `active` et `has_access`).

Anti-patterns courants

Du JSON sous forme de chaîne dans un champ JSON (`"metadata": "{\"plan\":\"pro\"}"` au lieu de `"metadata": { "plan": "pro" }`) force chaque client à analyser deux fois et sacrifie la sûreté de type sans rien apporter. Le plus souvent, le serveur a sérialisé telle quelle une colonne blob de base de données au lieu de la décoder avant de répondre.

Des formats de dates incohérents (certains champs en horodatages Unix, d'autres en `"08/15/2026"`, d'autres en ISO 8601 complet) obligent les clients à analyser chaque champ de date à part au lieu d'utiliser un seul gestionnaire partagé. Standardisez partout sur ISO 8601 en UTC avec un décalage explicite (`"2026-08-15T09:30:00Z"`).

Faire fuiter des champs internes de base de données, c'est renvoyer tels quels dans une réponse d'API des artefacts d'ORM comme `password_hash`, des clés étrangères internes destinées à un autre service, ou des champs de comptabilité (`__v`, `_id`, `created_by_worker_id`). Mappez votre modèle de base de données vers un DTO de réponse explicite plutôt que de sérialiser le modèle. Un DTO est le moyen fiable d'empêcher qu'un changement de schéma interne devienne un changement d'API public.

FAQ

Les réponses d'erreur doivent-elles utiliser la même enveloppe que les réponses de succès ?
Oui. Si les succès sont `{ "data": ..., "meta": ... }`, les erreurs devraient garder cette forme de haut niveau avec `data: null` et un objet `error` rempli. Le code de traitement des réponses du client vérifie alors une seule chose, la présence de `error`, sans se brancher sur la forme de la réponse selon le code de statut.
camelCase ou snake_case : le choix importe-t-il ?
La convention elle-même compte peu ; la cohérence compte énormément. Choisissez une convention pour toute l'API selon l'écosystème de vos principaux consommateurs, documentez-la et appliquez-la partout, y compris aux préfixes booléens et aux clés d'objets imbriqués. Le coût vient d'une API où chaque point de terminaison suit sa propre convention et où chaque client a besoin d'une logique de correspondance spécifique.
Une nouvelle API doit-elle utiliser la pagination par curseur ou par décalage ?
Privilégiez la pagination par curseur, sauf si vous devez spécifiquement permettre aux utilisateurs de sauter à un numéro de page arbitraire sur des données petites et majoritairement statiques. La pagination par curseur reste correcte quand des lignes sont ajoutées ou supprimées en cours de pagination et reste bon marché à toute profondeur ; la pagination par décalage se dégrade en correction comme en performance à mesure que le jeu de données grossit ou change.
Pourquoi ne pas sérialiser directement mon modèle de base de données comme réponse d'API ?
Votre schéma de base de données et votre contrat d'API public évoluent pour des raisons différentes et à des rythmes différents. Si vous sérialisez le modèle directement, chaque migration, colonne ajoutée ou mise à niveau d'ORM peut changer ou faire fuiter la forme de votre réponse publique sans que personne ne le remarque. Un DTO de réponse explicite découple les deux, au prix d'une étape de correspondance supplémentaire par point de terminaison.

Essayez ces outils

Articles associés