IFT3225

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 :

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.

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.

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.

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.