Aller au contenu

Concevoir une API

Application Lino (Boite à livres)

Il y a une tentation, quand on apprend à construire une API, de commencer par la base de données. On dessine ses tables, on les expose une par une, et on appelle ça du REST. Le résultat fonctionne — mais il ne parle pas. Chaque endpoint est une fenêtre sur une table, et c’est au client de recoller les morceaux pour comprendre quoi que ce soit.

Ce guide prend le chemin inverse. On part d’un système réel — un réseau de boîtes à livres — et on remonte des besoins des gens jusqu’au code. Le principe à retenir : une bonne API ne décrit pas une base de données, elle offre un abstraction permettant de répondre efficacement à des questions.

Lino est un réseau de boîtes à livres en libre-service disséminées dans une ville. Vous en avez probablement croisé : une petite armoire vitrée au coin d’une rue, remplie de romans que des inconnus déposent et prennent librement. Pas de bibliothécaire, pas de carte de membre, pas de date de retour. La confiance est implicite.

Avant d’écrire une seule ligne de code, il faut comprendre ce que les gens font déjà, sans application.

Quelqu’un marche dans son quartier, tombe sur une boîte, feuillette, prend un livre, en laisse un autre. Quelqu’un d’autre cherche un titre précis et fait le tour des boîtes à pied, sans jamais savoir si le livre qu’il veut se trouve à trois rues de là. Un troisième dépose un roman qu’il a adoré en espérant secrètement que quelqu’un le trouvera.

L’application ne doit pas remplacer ces gestes. Elle doit les amplifier — rendre visible ce qui était invisible, sans alourdir ce qui marchait déjà. C’est une contrainte de conception avant d’être une contrainte technique.

Quand on conçoit à partir des tables, on se demande « quelles entités ai-je à stocker? ». Quand on conçoit à partir des usages, on se demande « quelles questions les gens se posent? ». Les deux produisent des systèmes très différents.

Voici les questions que pose un utilisateur de Lino :

  • Où sont les boîtes près de moi, et est-ce qu’elles valent le détour?
  • Qu’est-ce qu’il y a dans cette boîte devant laquelle je suis?
  • Est-ce que quelqu’un, quelque part, a le livre que je cherche?
  • Je viens de déposer un livre — qui pourrait le vouloir?
  • D’où vient ce livre que je tiens? Combien de mains l’ont eu avant moi?

Chacune de ces questions deviendra un flux — une séquence d’échanges entre le client et le serveur. Et c’est là le cœur de l’affaire : un protocole n’est pas une liste d’endpoints, c’est une chorégraphie. Un endpoint isolé répond à une question; une séquence d’endpoints raconte une interaction.

Chaque flux ci-dessous se lit à trois niveaux. Commencez par le scénario — ce qu’un humain veut accomplir. Passez ensuite à la correspondance REST — comment ce besoin se traduit en ressources et en méthodes. Terminez par le flux intégré — la séquence complète, avec le code et les réponses.

01

Trouver des boîtes à proximité

Clara — citoyenne

Voir les boîtes autour de soi, avec assez d'information pour décider de s'y rendre ou non.

Clara ouvre l’application en marchant dans le Plateau. Elle veut savoir s’il y a des boîtes à livres autour d’elle — et si elles contiennent quelque chose d’intéressant. Elle ne veut pas se déplacer pour rien.

L’app utilise sa position GPS et affiche les boîtes proches avec leur distance, leur activité récente, et une estimation du contenu.

Remarquez ce que la réponse fait : elle ne renvoie pas des coordonnées brutes à charge pour Clara de calculer une distance. Elle renvoie "450m", "il y a 2 heures", "12 livres estimés" — de l’information déjà interprétée. La réponse fait le travail à la place du client. C’est ce qui sépare une API qui parle d’une API qui déverse.

02

Déposer un livre et prévenir ceux qui le cherchent

Marc — donneurSystèmeSophie — chercheuse

Marc signale un dépôt. Sophie veut être prévenue quand le livre qu'elle cherche apparaît.

Marc est devant la boîte du parc Laurier. Il dépose un exemplaire de Dune. Quelque part dans le quartier, Sophie cherche ce livre depuis deux semaines.

Marc scanne le code QR et confirme le dépôt. Le système vérifie si quelqu’un cherche ce titre. Sophie, la prochaine fois qu’elle consulte sa recherche, voit que Dune a été trouvé à 450m de chez elle.

Ce flux cache quelque chose d’important. Le POST de Marc ne se contente pas d’enregistrer un livre — il modifie l’état d’une autre ressource : la recherche de Sophie. Un seul appel déclenche une cascade côté serveur. Et la notification de Sophie n’arrive pas par magie : elle doit revenir consulter sa recherche. C’est du polling, et c’est exactement là où le modèle requête-réponse montre ses limites. Sophie aimerait être prévenue, mais REST ne sait pas pousser — il sait seulement répondre quand on demande. On y reviendra.

03

Suivre le parcours d'un livre

Léa — curieuse

Voir l'histoire d'un livre : son parcours à travers les boîtes de la ville.

Léa a trouvé un livre avec un code QR sur la couverture. Elle le scanne par curiosité : qui l’a lu avant elle? D’où vient-il? Combien de boîtes a-t-il traversées?

L’app affiche le parcours complet : chaque boîte où il est passé, les dates, la distance totale parcourue.

Cet endpoint est le plus révélateur des trois. Il n’y a aucune table « parcours » dans la base. Le parcours n’existe pas comme donnée — il est calculé à la volée à partir d’une suite d’événements bruts (des dépôts et des retraits). L’endpoint prend des faits dispersés et en fait un récit. C’est ce qu’on appelle une vue dérivée : la donnée stockée et la donnée servie ne sont pas la même chose.

Simulateur d'echange REST
Client
GET
Serveur
RequeteGET/boites?lat=45.523&lon=-73.587&rayon=2km
En-tetes
Accept
Traitement serveur
  1. Lire lat, lon, rayon
  2. Interroger l'index 2dsphere base
  3. Calculer les distances et trier
Reponse
Lance la requete pour voir la reponse.
Méthode Endpoint Description
GET /boites?lat=...&lon=...&rayon=... Boîtes à proximité avec distance et activité
GET /boites/:id Détail d’une boîte
POST /boites Ajouter une boîte
Méthode Endpoint Description
GET /boites/:id/livres Livres dans une boîte
POST /boites/:id/livres Déposer un livre
DELETE /boites/:id/livres/:code_qr Signaler qu’on prend un livre
GET /livres/:code_qr Parcours complet d’un livre
Méthode Endpoint Description
POST /recherches Créer une alerte
GET /recherches/:id Vérifier l’état
DELETE /recherches/:id Annuler
Méthode Endpoint Description
POST /boites/:id/signalements Signaler un problème
GET /boites/:id/signalements Voir les signalements
Méthode Endpoint Description
POST /echanges Proposer un échange
GET /echanges?destinataire=... Voir ses propositions
PUT /echanges/:id Accepter ou refuser
GET /echanges/:id Consulter l’état

Les endpoints et les flux déterminent le modèle — pas l’inverse. La question est : quelles lectures sont les plus fréquentes?

// Collection : boites
{
_id: "boite-14",
nom: "Boîte du parc Laurier",
position: { type: "Point", coordinates: [-73.585, 45.527] },
description: "Grande boîte vitrée, côté est du parc",
statut: "active",
derniere_activite: ISODate("2026-05-12T14:20:00Z"),
livres_estimes: 12
}
// Collection : evenements
{
_id: ObjectId("..."),
type: "depot", // "depot" | "retrait"
code_qr: "QR-7890",
titre: "Dune",
auteur: "Frank Herbert",
boite_id: "boite-14",
date: ISODate("2026-05-12T14:20:00Z")
}
// Collection : recherches
{
_id: "rech-5",
titre: "Dune",
statut: "trouvé", // "en attente" | "trouvé" | "annulée"
correspondances: [
{ boite_id: "boite-14", code_qr: "QR-7890", date: ISODate("...") }
]
}
// Collection : signalements
{
_id: ObjectId("..."),
boite_id: "boite-14",
type: "vandalisée",
note: "vitre cassée",
date: ISODate("2026-05-10T08:30:00Z")
}
// Collection : echanges
{
_id: "ech-3",
de: "nadia",
destinataire: "lea",
offre: { code_qr: "QR-1111", titre: "L'Étranger" },
demande: { code_qr: "QR-7890", titre: "Dune" },
message: "Intéressée par Dune — j'ai L'Étranger",
statut: "accepté",
rendez_vous: "Café Olimpico, 16h",
date: ISODate("2026-05-13T10:00:00Z")
}

Les livres dans une boîte pourraient être stockés dans le document de la boîte (un tableau livres). Mais chaque livre a un cycle de vie indépendant — il traverse plusieurs boîtes. Si les livres sont imbriqués, le parcours d’un livre (GET /livres/QR-7890) demande de chercher dans tous les documents boîtes.

En séparant les événements, le parcours d’un livre est une simple requête :

db.evenements.find({ code_qr: "QR-7890" }).sort({ date: 1 })

Les livres actuellement dans une boîte se déduisent des événements : les dépôts sans retrait correspondant. C’est un calcul, pas un stockage — et c’est un choix de conception (stocker l’état courant vs le recalculer à partir de l’historique).

db.boites.createIndex({ position: "2dsphere" }); // recherche géospatiale
db.evenements.createIndex({ code_qr: 1, date: 1 }); // parcours d'un livre
db.evenements.createIndex({ boite_id: 1, type: 1 }); // livres dans une boîte
db.recherches.createIndex({ titre: 1, statut: 1 }); // correspondances lors d'un dépôt

Voici l’endpoint de dépôt (créé avec Express) qui déclenche la cascade du flux 2 :

router.post("/boites/:id/livres", async (req, res) => {
const { titre, code_qr } = req.body;
// 1. Valider — avant de toucher la base
if (!titre || !code_qr) {
return res.status(400).json({
error: "MISSING_FIELDS",
message: "Les champs 'titre' et 'code_qr' sont requis.",
received: Object.keys(req.body),
});
}
// 2. Vérifier que la boîte existe
const boite = await db.collection("boites").findOne({ _id: req.params.id });
if (!boite) {
return res.status(404).json({
error: "BOITE_NOT_FOUND",
message: `La boîte '${req.params.id}' n'existe pas.`,
});
}
// 3. Enregistrer l'événement
await db.collection("evenements").insertOne({
code_qr, titre, action: "depose",
boite_id: req.params.id, date: new Date(),
});
// 4. Effet de bord : prévenir ceux qui cherchent ce titre
const notif = await db.collection("recherches").updateMany(
{ titre: new RegExp(titre, "i"), statut: "en attente" },
{ $set: { statut: "trouve" } }
);
// 5. Répondre — avec des liens vers la suite
res.status(201).json({
message: "Livre déposé.",
recherches_notifiees: notif.modifiedCount,
_links: { livre: `/livres/${code_qr}`, boite: `/boites/${req.params.id}` },
});
});

Tout ce qu’on a vu dans le cours est là, condensé : la validation avant l’action, les codes de statut qui portent un verdict (400, 404, 201), les erreurs expressives qui disent au client quoi corriger, l’effet de bord sur une autre ressource, et le champ _links qui guide le client vers la prochaine étape sans qu’il ait à deviner les URLs.


Quelques lectures qui prolongent la réflexion sur la conception d’API — moins sur la syntaxe, plus sur les intentions derrière les choix.

  • RESTful API Design: nouns are good, verbs are bad (Google Cloud) — le billet classique sur la différence entre nommer des ressources et nommer des actions, et pourquoi le premier produit des API prévisibles.

  • REST APIs must be hypertext-driven (Roy Fielding) — l’inventeur de REST explique, avec une certaine exaspération, ce que la plupart des « API REST » ratent : l’hypermédia comme moteur de l’état. Court, tranchant, fondateur.

  • Roy Fielding’s Misappropriated REST Dissertation (Two-Bit History) — une histoire fascinante de comment le mot « REST » a dérivé de sa définition d’origine. Utile pour comprendre pourquoi tant de débats sur « est-ce vraiment du REST » tournent en rond.

  • REST, Explained for Beginners (htmx) — une relecture du chapitre 5 de la thèse de Fielding pour des développeurs non-académiques. Défend l’idée que l’hypermédia (HTML, justement) est plus proche de l’esprit REST que les API JSON modernes.

  • Web API Design Best Practices (Microsoft Azure) — un guide complet et pragmatique sur la modélisation des ressources, le filtrage, la pagination et le versionnage. Une bonne référence à garder sous la main.

  • Most RESTful APIs aren’t really RESTful (Florian Krämer) — une discussion accessible de ce que le découplage client-serveur via l’hypermédia apporte réellement, au-delà du débat « verbes ou noms ».

L’idée centrale à retenir est qu’une API n’est pas simplement une copie de votre base de données rendue accessible en ligne. Une API est une interface : elle définit une façon claire et contrôlée d’interagir avec votre système. Son rôle est de permettre à d’autres développeurs, applications ou services de comprendre ce qu’ils peuvent demander à votre système et comment l’utiliser, sans avoir besoin d’entrer dans les détails de son code ou de sa structure interne.