REST API design - 16 - Relations entre ressources : embarquer, lier ou les deux

Sub-resources, expand/include, embedding vs linking, N+1 au niveau API et sparse fieldsets pour des réponses sur mesure.

REST API design 17 / 25
  1. 01 REST API design - 00 - Pourquoi le design de ton API change tout
  2. 02 REST API design - 01 - Les principes REST que personne ne lit
  3. 03 REST API design - 02 - Des URLs qui ont du sens
  4. 04 REST API design - 03 - Les méthodes HTTP, pour de vrai
  5. 05 REST API design - 04 - Les codes de statut HTTP qu'il faut connaître
  6. 06 REST API design - 05 - Body, headers et le diable dans les détails
  7. 07 REST API design - 06 - La pagination, ou comment ne pas tuer ta base
  8. 08 REST API design - 07 - Filtrage et tri sans prise de tête
  9. 09 REST API design - 08 - La validation avec Zod, gardien de ton API
  10. 10 REST API design - 09 - Erreurs : un format que tes clients vont adorer
  11. 11 REST API design - 10 - Authentification : JWT, API keys et OAuth2
  12. 12 REST API design - 11 - Versioning : quand et comment faire évoluer ton API
  13. 13 REST API design - 12 - CORS : comprendre et debugger les erreurs cross-origin
  14. 14 REST API design - 13 - Rate limiting : protéger ton API sans frustrer tes clients
  15. 15 REST API design - 14 - Caching : les bonnes réponses sont celles qu'on n'envoie pas
  16. 16 REST API design - 15 - Upload de fichiers : multipart, signed URLs et streaming
  17. 17 REST API design - 16 - Relations entre ressources : embarquer, lier ou les deux
  18. 18 REST API design - 17 - HATEOAS : des liens dans tes réponses
  19. 19 REST API design - 18 - OpenAPI : le contrat entre ton API et le monde
  20. 20 REST API design - 19 - Documentation : une API non documentee est une API inutile
  21. 21 REST API design - 20 - Testing : tester ton API sans devenir fou
  22. 22 REST API design - 21 - Webhooks : quand c'est ton API qui appelle
  23. 23 REST API design - 22 - Performance : quand chaque milliseconde compte
  24. 24 REST API design - 23 - Sécurité : les attaques que tu vas subir (et comment t'en protéger)
  25. 25 REST API design - 24 - Glossaire : tous les termes REST API en un seul endroit

16 - Relations entre ressources : embarquer, lier ou les deux

Ce que tu vas apprendre

  • Quand utiliser des sub-resources et quand utiliser des identifiants
  • Le pattern expand/include pour charger les relations a la demande
  • La différence entre embedding et linking
  • Le problème N+1 au niveau API et comment l'éviter
  • Les sparse fieldsets pour ne retourner que les champs nécessaires

Prerequisites

Avoir lu l'article sur l'upload de fichiers. Connaitre les bases du design de ressources REST (articles 1 a 5 de cette serie).


Tu as un endpoint /orders/42 qui retourne une commande. La commande a un client, des produits, une adresse de livraison, un moyen de paiement. Est-ce que tu embarques tout dans la réponse ? Est-ce que tu retournes juste les IDs et tu laisses le front faire 5 requêtes supplementaires ? Les deux approches ont des problèmes. La bonne réponse est entre les deux.

Sub-resources

Une sub-resource est une ressource qui n'a de sens que dans le contexte de son parent :

GET /orders/42/items          -- les items de la commande 42
GET /users/7/addresses        -- les adresses du user 7
POST /products/13/reviews     -- ajouter un avis au produit 13

La regle : si la ressource enfant n'existe pas indépendamment du parent, c'est une sub-resource. Un item de commande sans commande n'a pas de sens. Par contre, un produit existe indépendamment d'une commande. Donc /orders/42/products est discutable, /products/13 est mieux.

Ne descends pas a plus de deux niveaux. /users/7/orders/42/items/3/options est un signal que ton design a besoin d'etre repense. Utilise des top-level resources : /order-items/3.

Embedding vs linking

Linking : tu retournes les identifiants et le client fait des requêtes supplementaires.

json{
  "id": 42,
  "customer_id": 7,
  "item_ids": [101, 102, 103]
}

Embedding : tu inclus les donnees directement dans la réponse.

json{
  "id": 42,
  "customer": {
    "id": 7,
    "name": "Alice"
  },
  "items": [
    { "id": 101, "product": "Burger", "quantity": 2 },
    { "id": 102, "product": "Frites", "quantity": 1 }
  ]
}

Le linking est leger mais généré le problème N+1. L'embedding est complet mais peut devenir énorme si les relations sont profondes. Sur paltemps.fr, j'utilise l'embedding par défaut pour les relations directes et le linking pour le reste.

Le problème N+1 au niveau API

Avec le linking, un front qui affiche une liste de 20 commandes fait :

1 requete : GET /orders (20 commandes)
20 requetes : GET /users/{id} (un par commande)
20 requetes : GET /orders/{id}/items (items par commande)

41 requêtes pour afficher une page. C'est le problème N+1, mais au niveau HTTP au lieu de SQL. La latence s'accumule, surtout sur mobile.

Le pattern expand/include

La solution : laisser le client choisir quelles relations embarquer via un query parameter.

GET /orders?expand=customer,items

Sans expand, tu retournes les IDs. Avec expand, tu embarques les objets complets :

json{
  "data": [
    {
      "id": 42,
      "customer": { "id": 7, "name": "Alice", "email": "alice@example.com" },
      "items": [
        { "id": 101, "product_id": 13, "quantity": 2, "price": 1200 }
      ]
    }
  ]
}

L'implementation cote serveur :

typescriptapp.get("/orders", async (req, res) => {
  const expands = (req.query.expand as string)?.split(",") || [];

  let query = db.select().from(orders);

  if (expands.includes("customer")) {
    query = query.leftJoin(customers, eq(orders.customerId, customers.id));
  }

  if (expands.includes("items")) {
    // Charge les items en une seule requete, pas en N requetes
  }

  const result = await query;
  res.json({ data: result });
});

Limite les relations expandables. Ne laisse pas le client faire expand=customer.orders.items.product.reviews. Definis une liste blanche et une profondeur max.

Sparse fieldsets

Parfois tu n'as pas besoin de toute la ressource. Un select sur les champs :

GET /users?fields=id,name,avatar
GET /orders?fields=id,total,status&expand=customer&fields[customer]=id,name

JSON:API formalise ca avec fields[type] :

GET /orders?include=customer&fields[orders]=id,total&fields[customers]=id,name

Ca réduit drastiquement la taille des réponses. Sur une liste de 100 users, retourner seulement id, name et avatar au lieu de 20 champs peut diviser la taille par 5.

typescriptapp.get("/users", async (req, res) => {
  const fields = (req.query.fields as string)?.split(",") || ["*"];

  const allowedFields = ["id", "name", "email", "avatar", "created_at"];
  const selectedFields = fields.filter(f => allowedFields.includes(f));

  const users = await db
    .select(selectedFields.map(f => users[f]))
    .from(users);

  res.json({ data: users });
});

Batch endpoints

Pour résoudre le N+1 sans expand, tu peux proposer un endpoint batch :

GET /users?ids=7,12,34,56

Le front récupéré ses 20 commandes, collecte les customer_id uniques, et fait une seule requête batch. Deux requêtes au total au lieu de 21.

typescriptapp.get("/users", async (req, res) => {
  const ids = (req.query.ids as string)?.split(",").map(Number);

  if (ids && ids.length > 100) {
    return res.status(400).json({
      detail: "Maximum 100 IDs par requete batch."
    });
  }

  const users = await db.select().from(usersTable).where(inArray(usersTable.id, ids));
  res.json({ data: users });
});

Quelle stratégie choisir

Situation Approche
Relation toujours nécessaire Embedding par défaut
Relation parfois nécessaire expand/include
Liste avec beaucoup de champs Sparse fieldsets
Front qui agrege plusieurs ressources Batch endpoints
Relation profonde ou circulaire Linking avec IDs

Résumé

  • Les sub-resources modelisent les relations parent-enfant (max 2 niveaux)
  • Le pattern expand/include laisse le client choisir les relations a embarquer
  • Les sparse fieldsets reduisent la taille des réponses en selectionnant les champs
  • Les batch endpoints resolvent le N+1 en permettant de charger plusieurs ressources en une requête
  • Combine ces approches selon le cas d'usage plutot que d'en choisir une seule

Article précédent : Upload de fichiers Article suivant : HATEOAS

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.