Jusqu’ici, nous avons construis des applications web complètent, mais fermés
sur eux même. Ici, nous voyons comment ouvrir son application à d’autres programmes, de façon
organisée et prévisible. Ceci permettra de centraliser la logique métier et l’accès
aux données tout en laissant flexible l’exploitation et la présentation de ceux-ci.
On change également d’outil pour construire les APIs: l’API sera écrite en JavaScript, avec Node.js et Express.
Revenons à notre exemple du catalogue de L’Érablière dorée.
Nous avons construit un site web présentant ses produits, mais ce n’est pas nécessairement
le seule client qui pourrait présenter ou exploiter le catalogue.
Une application mobile pourrait également afficher les produits
un partenaire pourrait vouloir les revendre,
un script d’un administrateur système pourrait voiloir mettre les stocks à jour.
Sans API, chacun devrait soit accéder directement à la base de données,
soit extraire les données du HTML des pages, ce qui n’est pas commode.
De plus, le moindre changement au niveau de la base de données ou dans le
rendu pourrait briser les clients.
Une API vient répondre à quelques besoins récurrents
Les principes REST décrivent un style architectural
pour concevoir des systèmes distribués, notamment des API Web.
REST définit plusieurs contraintes architecturales. Pour concevoir notre première API, nous en retiendrons principalement trois.
Communication client–serveur : le client gère principalement la présentation et les interactions; le serveur gère les ressources, les données et la logique associée. Cette séparation permet aux deux d’évoluer plus indépendamment.
Sans état (stateless) :chaque requête contient toute l’information nécessaire pour être comprise et traitée. Le serveur ne dépend pas d’un état de conversation conservé entre deux requêtes d’un même client.
Interface uniforme : les ressources sont manipulées suivant des conventions communes et prévisibles. C’est probablement la contrainte la plus importante de REST. Elle implique notamment :
l’identification des ressources, généralement par des URI;
la manipulation des ressources à travers leurs représentations (JSON, XML, etc.);
des messages suffisamment explicites pour être compris;
idéalement, des liens permettant au client de découvrir les actions ou ressources accessibles (HATEOAS).
Si nous appliquons REST au catalogue de l’Érablière, nous obtenons les cinq points d’accès suivants.
Méthode
Adresse
Action
Réponse en cas de succès
GET
/api/produits
lister les produits
200, la liste
GET
/api/produits/7
lire un produit
200, le produit
POST
/api/produits
créer un produit
201, le produit, et son adresse dans Location
PUT
/api/produits/7
remplacer un produit
200, le produit modifié
DELETE
/api/produits/7
supprimer un produit
204, sans corps
Et quatre réponses d’erreur, chacune pour une raison distincte.
Statut
Signification
Exemple
400
la requête est illisible
le corps n’est pas du JSON valide
404
la ressource n’existe pas
/api/produits/99, ou /api/clients
405
la méthode ne convient pas à cette ressource
DELETE /api/produits, sur la collection entière
422
la requête est lisible, mais refusée
un prix négatif
VérificationPourquoi préférer DELETE /api/produits/7 à GET /api/supprimerProduit?id=7 ?
Pour deux raisons. La première est la clarté du contrat : l’adresse désigne toujours la même ressource, et la méthode seule indique l’action, si bien qu’un client qui connaît /api/produits/7 sait déjà comment la lire, la remplacer et la supprimer. La seconde est la sécurité, déjà rencontrée avec les formulaires : une requête GET peut être déclenchée sans intention, par un préchargement ou un robot, et ne devrait rien modifier.
Node.js s’installe depuis nodejs.org, en version LTS (Long Term Support, la version maintenue le plus longtemps).
XAMPP n’intervient pas ici : Node.js est lui-même le serveur.
Fenêtre de terminal
mkdirerabliere-api
cderabliere-api
npminit-y# crée package.json
npminstallexpress# télécharge Express
Deux ajouts au package.json créé par npm init :
{
"name": "erabliere-api",
"version": "1.0.0",
"type": "module",
"main": "src/serveur.js",
"scripts": {
"dev": "node --watch src/serveur.js",
"start": "node src/serveur.js"
},
"dependencies": {
"express": "^5.3.0"
}
}
"type": "module" permet d’écrire import et export, la syntaxe des modules qu’on retrouvera avec Vue.js.
Le script dev lance le serveur avec --watch : Node.js le redémarre à chaque modification d’un fichier. On le lance par npm run dev.
express() crée l’application, qui recevra toutes les requêtes.
app.get(adresse, fonction) déclare une route : pour une requête GET à cette adresse, Express appelle la fonction avec deux objets, la requête req et la réponse res.
res.json() convertit l’objet en JSON, pose l’en-tête Content-Type: application/json et envoie la réponse.
app.listen(3000) ouvre le port 3000 et attend les requêtes.
VérificationLe serveur tourne. Vous modifiez le message de la route, sans utiliser --watch, puis vous rechargez la page. Pourquoi voyez-vous encore l'ancien message ?
Parce que le programme a lu le fichier au démarrage, et qu’il continue de s’exécuter avec cette version. En PHP, chaque requête relit le fichier, d’où l’effet immédiat d’une modification. Avec Node.js, il faut redémarrer le serveur ; l’option --watch le fait automatiquement à chaque enregistrement.
déclarer une route pour une méthode et une adresse
req.params.id
la partie de l’adresse désignée par :id, toujours sous forme de texte
req.body
le corps JSON de la requête, déjà décodé
res.status(404)
choisir le code de statut ; se combine avec la suite
res.json(objet)
envoyer l’objet en JSON
res.location(adresse)
poser l’en-tête Location
res.sendStatus(204)
envoyer un statut seul, sans corps
Par défaut, Express ne lit pas le corps d’une requête. La ligne app.use(express.json()), placée avant les routes, lui demande de lire le JSON reçu et de le ranger dans req.body.
src/serveur.js
Étape 1 : les données, les cinq points d'accès et le démarrage, dans un seul fichier.
La spécification
Garde les six produits de départ en mémoire, et attribue un identifiant à chaque nouveau produit.
Collection : liste en lecture ; création, réussie avec 201 et l’adresse du nouveau produit.
Produit : lecture, remplacement, suppression ; 404 s’il n’existe pas.
Suppression réussie : 204, sans corps.
Écoute sur le port 3000.
Les étapes de résolution
Survolez une étape pour voir le code correspondant. Un clic l'épingle.
Les requêtes prévues fonctionnent. Mais une API ne choisit pas ses clients : n’importe quel programme peut lui envoyer n’importe quoi. Voici ce que répond l’étape 1 à quatre requêtes moins sages.
Requête
Réponse de l’étape 1
POST avec {"nom": "Pastilles"}
201 : un produit sans catégorie, sans prix ni stock, est créé
POST avec un prix de -3 et la catégorie fromage
201 : le produit est créé tel quel
POST sans corps
500, et une page d’erreur HTML : req.body vaut undefined, et la route plante
POST avec {"nom":
400, mais sous forme de page HTML, illisible pour un programme
L’étape 1 croit tout ce que le client lui envoie.VérificationLes pages HTML du catalogue avaient des champs obligatoires et un champ de prix numérique. Pourquoi ces protections ne suffiraient-elles pas pour l'API ?
Parce qu’elles vivent dans le formulaire, donc chez le client. Une API ne reçoit pas ses requêtes d’un formulaire qu’elle a elle-même produit : elle les reçoit de curl, d’un script, d’une application ou d’une page écrite par d’autres. La seule vérification sur laquelle le serveur peut compter est la sienne. C’était déjà vrai pour les pages, où un formulaire modifié dans le navigateur contourne les attributs required.
Les quatre requêtes de l’épreuve révèlent deux sortes de problèmes, qui appellent deux statuts différents :
400 : le corps est absent, illisible, ou n’est pas un objet JSON. L’API ne peut même pas lire la demande.
422 : le corps est un objet lisible, mais ses valeurs sont refusées : nom vide, catégorie inconnue, prix négatif. L’API répond avec les anomalies par champ, sous la clé erreurs.
Deux fonctions s’ajoutent en tête du fichier, après les données. Les règles sont celles du guide sur la base de données.
La route PUT reçoit les mêmes vérifications, après le 404 : on vérifie d’abord que le produit existe, puis ce que le client propose de mettre à sa place.
Le fichier complet après l’étape 2
import express from'express';
const app = express();
// Permet de lire un corps JSON dans req.body
app.use(express.json());
// Les produits, gardés en mémoire tant que le serveur tourne
L’API fonctionne et est encore maintenable. Cependant, serveur.js compte déjà 125 lignes, et plusieurs signes montrent qu’il ne tiendra pas longtemps ainsi.
Il fait quatre choses à la fois : il garde les données, applique les règles, déclare les routes et démarre le serveur. Chacune est une raison distincte de le modifier.
Les adresses se répètent : '/api/produits' est écrit deux fois, '/api/produits/:id' trois fois. Renommer la ressource, ou la placer sous /api/v1, demande de modifier chaque route.
La prochaine ressource s’y mêlera : les commandes, proposées en exercice plus bas, ajouteraient leurs routes et leurs règles au même fichier.
Le tableau passe dans son propre module. Il exporte les produits, la liste des catégories permises, et une fonction qui attribue les identifiants ; le compteur prochainId, lui, reste privé au module.
// Les produits du catalogue, gardés en mémoire.
// Ils sont partagés par toutes les requêtes, et reviennent à cet état
Les routes de l’étape 2 y passent presque telles quelles : les adresses perdent leur préfixe, et les recherches répétées deviennent une fonction trouver().
src/routes/produits.js
Les cinq points d'accès de l'API sur les produits.
La spécification
Traite les opérations de l’API sur la collection des produits et sur chaque produit.
Collection : liste en lecture ; création, réussie avec 201 et l’adresse du nouveau produit.
Produit : lecture, remplacement, suppression ; 404 s’il n’existe pas.
Création et remplacement : un objet JSON est exigé, sinon 400 ; données refusées : 422, avec les anomalies par champ.
Règles : nom obligatoire d’au plus 100 caractères, catégorie de la liste, prix positif, stock entier positif ou nul.
Suppression réussie : 204, sans corps.
Méthode non prévue pour une adresse connue : 405, avec les méthodes admises.
Les étapes de résolution
Survolez une étape pour voir le code correspondant. Un clic l'épingle.
OUTILS
functionvalider(donnees) {
const erreurs = {};
// nom, categorie, prix, stock : une règle par champ
console.log(`API du catalogue : http://localhost:${PORT}/api/produits`);
});
app.js crée l’application, serveur.js la démarre. Cette séparation peut sembler excessive pour quelques lignes. Elle permettra plus tard de tester l’application sans ouvrir de port, en important app.js seulement.
Ajoutez à l’API une seconde ressource, les commandes, en suivant l’organisation des produits : un module de données src/donnees/commandes.js, un routeur src/routes/commandes.js, branché dans app.js sur /api/commandes.
Le client envoie seulement client et lignes. Le serveur attribue l’identifiant et calcule le total à partir des prix des produits.
client est obligatoire. lignes est un tableau non vide ; chaque ligne désigne un produit existant, avec une quantité entière d’au moins 1. Toute anomalie produit un 422, avec les clés client ou lignes dans erreurs.
Toute autre méthode répond 405, avec l’en-tête Allow.
Une API met les opérations d’un logiciel à la disposition d’autres logiciels. Elle permet plusieurs clients, des rythmes d’évolution séparés et une seule mise en œuvre des règles.
Elle applique à l’échelle d’un système les principes déjà vus pour les classes : encapsulation, couplage faible, responsabilité unique. Une API est une interface : le client dépend de ce qu’elle promet, pas de la façon dont elle le tient.
Une API web se conçoit en répondant à une série de questions : adresse, méthode, format, en-têtes, statuts, erreurs, authentification, CORS, versionnement, documentation.
REST organise l’API autour de ressources désignées par des adresses ; la méthode HTTP porte l’action, chaque requête se suffit à elle-même, et le code de statut dit le résultat.
Node.js exécute du JavaScript sur le serveur ; un seul programme reste en marche et traite toutes les requêtes. Express fournit le routage et les objets req et res.
express.json() décode le corps JSON dans req.body ; req.params contient les parties variables de l’adresse, sous forme de texte.
Une API ne choisit pas ses clients : elle vérifie elle-même ce qu’elle reçoit. Un corps illisible ou absent mérite un 400 ; des valeurs refusées, un 422, avec les anomalies par champ.
Un routeur regroupe les routes d’une ressource ; app.js le branche sur un préfixe, puis traite les cas restants : 404, puis les erreurs. router.route() regroupe les méthodes d’une adresse, et .all() répond 405 aux autres.