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.
Le système
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.
On commence par les questions
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.
Trois flux, du besoin au code
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.
Trouver des boîtes à proximité
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.
- Ressource
/boites— les boîtes à livres- Action
GET— lire, pas modifier- Filtrage
?lat=45.523&lon=-73.587&rayon=2km- Route Express
router.get(“/boites”, handler)
La réponse inclut la distance calculée et l’activité récente — le client n’a pas à faire ces calculs lui-même.
GET /boites?lat=45.523&lon=-73.587&rayon=2kmLa position GPS est envoyée à l’ouverture de l’app.- Le serveur interroge MongoDB avec un index
2dsphere, calcule les distances et trie du plus proche au plus loin. Réponse
200:[{ "id": "boite-14", "nom": "Boîte du parc Laurier", "distance": "450m", "derniere_activite": "il y a 2 heures", "livres_estimes": 12, "_links": { "detail": "/boites/boite-14" } }]- Clara voit la boîte à 450m, active — elle s’y rend. Clic →
GET /boites/boite-14
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.
Déposer un livre et prévenir ceux qui le cherchent
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.
- Dépôt
POST /boites/:id/livres- Alerte
POST /recherches- Consultation
GET /recherches/:id- Effet de bord
- Le
POSTde Marc met à jour la recherche de Sophie — sans qu’elle agisse
Sophie crée son alerte (avant) :
POST /recherches→201 { id: "rech-5", statut: "en attente" }
Marc dépose :
POST /boites/boite-14/livresavec{ titre, code_qr }- Le serveur insère l’événement, met à jour la boîte, et cherche les recherches actives pour « Dune »
- Réponse
201avecrecherches_notifiees: 1
Sophie revient (après) :
GET /recherches/rech-5— même endpoint, réponse différenteRéponse
200:{ "statut": "trouvé", "resultats": [{ "boite": "boite-14", "distance": "450m" }] }
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.
Suivre le parcours d'un livre
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.
- Ressource
/livres/:code_qr- Action
GET— lecture seule- Données
- Agrégation des événements (dépôts + retraits), pas un document stocké tel quel
C’est un endpoint sémantique : il ne retourne pas une ligne de base de données, il raconte une histoire en agrégeant des événements.
GET /livres/QR-7890— Léa scanne le QR- Le serveur agrège :
db.evenements.find({ code_qr: "QR-7890" }).sort({ date: 1 }) Réponse
200:{ "titre": "Dune", "parcours": [ { "action": "déposé", "boite": "boite-7", "date": "2026-04-01" }, { "action": "pris", "boite": "boite-7", "date": "2026-04-18" }, { "action": "déposé", "boite": "boite-14", "date": "2026-05-12" } ], "distance_parcourue": "3.2km" }- Léa voit que le livre a voyagé 3.2km. Elle décide de le lire et de le redéposer ailleurs.
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.
/boites?lat=45.523&lon=-73.587&rayon=2kmAccept- Lire lat, lon, rayon
- Interroger l'index 2dsphere base
- Calculer les distances et trier
Table récapitulative des endpoints
Boîtes
| 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 |
Livres
| 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 |
Recherches
| Méthode | Endpoint | Description |
|---|---|---|
POST |
/recherches |
Créer une alerte |
GET |
/recherches/:id |
Vérifier l’état |
DELETE |
/recherches/:id |
Annuler |
Signalements
| Méthode | Endpoint | Description |
|---|---|---|
POST |
/boites/:id/signalements |
Signaler un problème |
GET |
/boites/:id/signalements |
Voir les signalements |
Échanges
| 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 |
Modèle de données
Les endpoints et les flux déterminent le modèle — pas l’inverse. La question est : quelles lectures sont les plus fréquentes?
Collections MongoDB
// 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")
}
Pourquoi des événements séparés?
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).
Index
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
Le code n’est que la dernière étape
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.
Pour aller plus loin
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.