Prise en main d'Express
Qu’est-ce qu’Express ?
Section intitulée « Qu’est-ce qu’Express ? »
Express est un cadriciel web minimaliste et flexible pour Node.js. Il aide à recevoir des requêtes HTTP et à construire les réponses d’une application Web ou d’une API.
-
Minimaliste ? Express ajoute une couche légère aux fonctionnalités HTTP de Node.js. Il fournit notamment le routage, les middlewares et des méthodes pratiques pour construire les réponses.
Flexible ? Express n’impose ni ORM ni structure de projet complète. Il faut donc choisir comment organiser les routes, l’accès aux données et les autres responsabilités. Cette liberté facilite le démarrage, mais elle exige aussi de prendre de bonnes décisions de conception.
Express dans son écosystème
Section intitulée « Express dans son écosystème »Express n’est pas le seul cadriciel serveur de l’écosystème JavaScript. Quelques solutions courantes adoptent des orientations différentes :
| Cadriciel | Orientation générale |
|---|---|
| Express | Noyau minimal, API simple et vaste écosystème de middlewares |
| Fastify | Performance, extensibilité et validation fondée sur des schémas |
| Koa | Petit noyau centré sur la composition de middlewares |
| NestJS | Architecture applicative plus structurée et davantage de conventions |
Ce qu’on va construire
Section intitulée « Ce qu’on va construire »Tout au long de ce guide, on construit le serveur d’une petite API pour un réseau de boîtes à livres, le même domaine que dans l’exemple de conception d’API.
L’objectif est d’exposer des boîtes à livres réparties dans la ville pour qu’une application puisse les lister, en consulter une, en ajouter, les mettre à jour et les supprimer.
On y arrive par étapes, en partant de presque rien. À chaque étape, on simule d’abord la requête à gérer, puis on écrit le code qui la traite. Voir l’échange avant de l’implémenter aide à garder en tête le résultat recherché. À la fin, on aura une petite API CRUD en mémoire, découpée proprement en modules.
Installation
Section intitulée « Installation »Express 5 exige Node.js 18 ou une version plus récente. Dans un nouveau dossier, initialisez le projet, activez les modules ES et installez Express :
npm init -ynpm pkg set type=modulenpm install expressÉtape 1 : le serveur minimal
Section intitulée « Étape 1 : le serveur minimal »Avant d’écrire quoi que ce soit, voici l’échange le plus simple possible :
Le client demande la racine
/, le serveur répond avec un peu de texte.
Lancez la simulation pour voir le trajet.
/- Express recoit la requete
- La route GET / correspond
- res.send() renvoie le texte
C’est tout ce qu’on veut pour l’instant : une requête GET /, une réponse 200 avec du texte. Voici le code qui produit exactement cet échange :
import express from "express";const app = express();app.get("/", (req, res) => { res.send("Bonjour depuis Express!");});app.listen(3000, () => { console.log("Serveur démarré sur http://localhost:3000");});Décortiquons les quatre gestes fondateurs :
-
express()crée l’application, l’objet central qui orchestre tout. -
app.get(chemin, gestionnaire)enregistre une route : « quand une requêteGETarrive sur ce chemin, exécute cette fonction ». -
(req, res) => { ... }est le gestionnaire (handler) associé à la requêteGET /. Il reçoit la requête (req) et construit la réponse (res). Ici,res.send()renvoie du texte. -
app.listen(port)démarre le serveur et le met à l’écoute sur un port.
Lancez avec node index.js, ouvrez dans un navigateur http://localhost:3000. Vous devriez voir le texte Bonjour depuis Express!
Ces instructions forment le squelette d’un serveur Express. La suite ajoute progressivement des routes et des traitements.
Étape 2 : répondre en JSON
Section intitulée « Étape 2 : répondre en JSON »Une API peut renvoyer plusieurs types de contenu. Ici, on veut exposer des données structurées avec une route GET /boites qui retourne un tableau de boîtes en JSON. Simulez l’échange :
/boites- La route GET /boites correspond
- Lire le tableau des boites base
- res.json() serialise et renvoie
La réponse n’est plus une phrase mais un tableau d’objets. Le code ajoute une route et utilise res.json() au lieu de res.send() :
import express from "express";const app = express();// Nos données, en mémoire pour l'instantconst boites = [ { id: 14, nom: "Boîte du parc Laurier", quartier: "Plateau" }, { id: 7, nom: "Coin Fabre / Saint-Zotique", quartier: "Rosemont" }, { id: 3, nom: "Place Saint-Hubert", quartier: "Plateau" },];app.get("/", (req, res) => { res.send("Bonjour depuis Express!");});app.get("/boites", (req, res) => { res.json(boites); // sérialise le tableau en JSON});app.listen(3000);Le fichier a maintenant deux routes. Express parcourt les gestionnaires dans leur ordre de déclaration. Un gestionnaire peut terminer la réponse ou transmettre le contrôle au suivant avec
next().
Étape 3 : un paramètre dans l’URL
Section intitulée « Étape 3 : un paramètre dans l’URL »Pour obtenir une boîte précise, on met son identifiant dans le chemin : GET /boites/14. Express capture cette partie variable comme un paramètre de route.
Mais que se passe-t-il si la boîte n’existe pas ? Simulez les deux cas avec
id=14(existe), puisid=99(n’existe pas) :
/boites/14- Extraire req.params.id
- Chercher la boite correspondante base
- Renvoyer la boite ou un 404
La simulation montre les deux issues : 200 avec la boîte, ou 404 si l’id ne correspond à rien.
Voici le code pour cette requête :
app.get("/boites/:id", (req, res) => { const id = Number(req.params.id); // :id arrive comme texte const boite = boites.find((b) => b.id === id); if (!boite) { return res.status(404).json({ error: "Boîte introuvable", id }); } res.json(boite);});Les points importants :
-
:iddans le chemin déclare un paramètre. Express le rend disponible dansreq.params.id. -
Les paramètres de route sont des chaînes de caractères :
req.params.idvaut"14", pas14. D’où leNumber(...). -
res.status(404)fixe le code de statut avant d’envoyer le corps. Sans ça, Express renvoie200par défaut. Le
returndevantres.status(404)est essentiel : il arrête le gestionnaire. Sans lui, le code continuerait et tenterait d’envoyer une deuxième réponse.
Étape 4 : filtrer avec des paramètres de requête
Section intitulée « Étape 4 : filtrer avec des paramètres de requête »Le paramètre de route (:id) identifie une ressource précise, mais souvent, on veut plutôt filtrer une liste : « les boîtes du Plateau ».
Pour ça, on n’invente pas nécessairement une nouvelle route. On ajoute des paramètres de requête (query parameters) à l’URL, après un ?.
Simulez GET /boites?quartier=Plateau (videz le champ pour tout obtenir) :
/boites?quartier=Plateau- Lire req.query.quartier
- Si absent, tout renvoyer
- Sinon filtrer le tableau base
- res.json() renvoie le sous-ensemble
C’est la même route GET /boites qu’à l’étape 2. On l’enrichit pour lire les paramètres de requête. Express les expose dans req.query :
app.get("/boites", (req, res) => { const quartier = typeof req.query.quartier === "string" ? req.query.quartier.trim() : ""; // Pas de filtre demandé : on renvoie tout if (!quartier) { return res.json(boites); } // Filtre demandé : on renvoie le sous-ensemble const filtrees = boites.filter( (b) => b.quartier.toLowerCase() === quartier.toLowerCase() ); res.json(filtrees);});Les points importants :
-
req.queryest un objet rassemblant tout ce qui suit le?. Pour?quartier=Plateau&tri=nom, on areq.query.quartieretreq.query.tri. -
Dans cette route,
quartierest optionnel : sans filtre, l’API renvoie la liste complète. Une autre API pourrait toutefois définir certains paramètres de requête comme obligatoires. -
Les valeurs proviennent de l’URL et ne doivent pas être utilisées sans validation. Selon le parseur et la forme de la requête, une valeur de
req.querypeut notamment être une chaîne ou un tableau. Ici, on accepte seulement une chaîne.
Un même endpoint peut servir plusieurs besoins grâce à ses paramètres de requête : tout lister, filtrer par quartier, puis éventuellement trier ou paginer, sans multiplier les routes.
Étape 5 : lire le corps d’une requête POST
Section intitulée « Étape 5 : lire le corps d’une requête POST »Jusqu’ici, on a seulement lu des données. Pour en créer, le client envoie un POST avec un corps JSON.
Express ne transforme pas automatiquement un corps JSON en objet JavaScript. Il faut activer un middleware pour le faire. Simulez la création, puis essayez de vider le champ
nompour voir l’erreur400:
/boites- La route GET /boites correspond
- Lire le tableau des boites base
- res.json() renvoie
Après un
POSTréussi, relancez leGET /boitesdans la simulation : la nouvelle boîte y est.
Le code introduit deux nouveautés, express.json() et req.body :
// Active la lecture des corps JSON; à placer avant les routesapp.use(express.json());app.post("/boites", (req, res) => { const { nom, quartier } = req.body ?? {}; if (typeof nom !== "string" || !nom.trim()) { return res.status(400).json({ error: "Le champ 'nom' est requis." }); } const prochainId = Math.max(0, ...boites.map((boite) => boite.id)) + 1; const nouvelle = { id: prochainId, nom: nom.trim(), quartier: typeof quartier === "string" && quartier.trim() ? quartier.trim() : "Inconnu", }; boites.push(nouvelle); res.location(`/boites/${nouvelle.id}`).status(201).json(nouvelle);});Le statut 201 Created signale qu’une ressource a été créée. L’en-tête Location, ajouté ici avec res.location(), indique l’URL de la nouvelle ressource. Ces éléments rendent la réponse plus explicite pour le client.
Étape 6 : modifier et supprimer
Section intitulée « Étape 6 : modifier et supprimer »Il reste deux opérations pour couvrir le cycle de vie complet d’une ressource : remplacer une boîte existante (PUT) et la supprimer (DELETE).
La simulation regroupe les quatre verbes : créez, listez, modifiez avec PUT /boites/14, puis supprimez avec DELETE /boites/7 :
/boites- La route GET /boites correspond
- Lire le tableau des boites base
- res.json() renvoie
Le code ajoute les deux gestionnaires.
-
PUTressemble à unPOSTciblé par:id -
DELETEretire l’élément et répond204:
// Remplacer une boîte existanteapp.put("/boites/:id", (req, res) => { const id = Number(req.params.id); const boite = boites.find((b) => b.id === id); if (!boite) { return res.status(404).json({ error: "Boîte introuvable", id }); } const { nom, quartier } = req.body ?? {}; if (typeof nom !== "string" || !nom.trim()) { return res.status(400).json({ error: "Le champ 'nom' est requis." }); } boite.nom = nom.trim(); boite.quartier = typeof quartier === "string" && quartier.trim() ? quartier.trim() : "Inconnu"; res.json(boite); // 200 avec la version à jour});// Supprimer une boîteapp.delete("/boites/:id", (req, res) => { const id = Number(req.params.id); const index = boites.findIndex((b) => b.id === id); if (index === -1) { return res.status(404).json({ error: "Boîte introuvable", id }); } boites.splice(index, 1); res.status(204).end(); // 204 No Content : succès, rien à renvoyer});Les points à retenir :
-
PUTcombine un paramètre de route (quelle boîte) et un corps (les nouvelles valeurs). -
res.status(204).end(): le204 No Contentsignale un succès sans corps. On termine avec.end()plutôt que.json()car il n’y a rien à sérialiser.
Avec GET, POST, PUT et DELETE, on couvre les quatre opérations du CRUD : Create, Read, Update, Delete. Ce modèle est fréquent dans les API centrées sur des ressources, même si toutes les API ne se limitent pas au CRUD.
Le fichier devient trop gros
Section intitulée « Le fichier devient trop gros »À ce stade, index.js contient la création de l’app, les données, et toutes les routes.
Pour quatre routes, ça va. Mais une vraie API en a des dizaines, et tout empiler dans un fichier devient vite ingérable.
On va séparer les responsabilités en modules :
mon-serveur/├── index.js ← démarrage du serveur uniquement├── src/│ ├── app.js ← création de l'app + middlewares│ ├── data/│ │ └── boites.js ← les données (plus tard : la base)│ └── routes/│ └── boites.js ← toutes les routes /boites└── package.jsonL’idée directrice : chaque fichier a une seule raison de changer.
Le routeur : src/routes/boites.js
Section intitulée « Le routeur : src/routes/boites.js »Express fournit un objet Router, un système de routage modulaire qui regroupe des routes liées et que l’on monte ensuite dans l’application principale.
import { Router } from "express";import { boites } from "../data/boites.js";const router = Router();function lireId(valeur) { const id = Number(valeur); return Number.isInteger(id) && id > 0 ? id : null;}router.get("/", (req, res) => { const quartier = typeof req.query.quartier === "string" ? req.query.quartier.trim() : ""; const resultat = quartier ? boites.filter( (boite) => boite.quartier.toLowerCase() === quartier.toLowerCase() ) : boites; res.json(resultat);});router.get("/:id", (req, res) => { const id = lireId(req.params.id); if (id === null) { return res.status(400).json({ error: "L'identifiant doit être un entier positif." }); } const boite = boites.find((b) => b.id === id); if (!boite) { return res.status(404).json({ error: "Boîte introuvable", id }); } res.json(boite);});router.post("/", (req, res) => { const { nom, quartier } = req.body ?? {}; if (typeof nom !== "string" || !nom.trim()) { return res.status(400).json({ error: "Le champ 'nom' est requis." }); } const prochainId = Math.max(0, ...boites.map((boite) => boite.id)) + 1; const nouvelle = { id: prochainId, nom: nom.trim(), quartier: typeof quartier === "string" && quartier.trim() ? quartier.trim() : "Inconnu", }; boites.push(nouvelle); res.location(`${req.baseUrl}/${nouvelle.id}`).status(201).json(nouvelle);});router.put("/:id", (req, res) => { const id = lireId(req.params.id); if (id === null) { return res.status(400).json({ error: "L'identifiant doit être un entier positif." }); } const boite = boites.find((b) => b.id === id); if (!boite) { return res.status(404).json({ error: "Boîte introuvable", id }); } const { nom, quartier } = req.body ?? {}; if (typeof nom !== "string" || !nom.trim()) { return res.status(400).json({ error: "Le champ 'nom' est requis." }); } boite.nom = nom.trim(); boite.quartier = typeof quartier === "string" && quartier.trim() ? quartier.trim() : "Inconnu"; res.json(boite);});router.delete("/:id", (req, res) => { const id = lireId(req.params.id); if (id === null) { return res.status(400).json({ error: "L'identifiant doit être un entier positif." }); } const index = boites.findIndex((b) => b.id === id); if (index === -1) { return res.status(404).json({ error: "Boîte introuvable", id }); } boites.splice(index, 1); res.status(204).end();});export default router;Les données : src/data/boites.js
Section intitulée « Les données : src/data/boites.js »export const boites = [ { id: 14, nom: "Boîte du parc Laurier", quartier: "Plateau" }, { id: 7, nom: "Coin Fabre / Saint-Zotique", quartier: "Rosemont" }, { id: 3, nom: "Place Saint-Hubert", quartier: "Plateau" },];Isoler les données prépare le terrain pour la suite, mais ce tableau n’est pas encore une véritable couche d’accès aux données. Le passage à MongoDB rendra notamment les opérations asynchrones et demandera d’adapter les routes. Plus tard, une couche de service ou de dépôt permettra de mieux isoler ce changement.
L’assemblage : src/app.js
Section intitulée « L’assemblage : src/app.js »import express from "express";import boitesRouter from "./routes/boites.js";const app = express();app.use(express.json());// Monte le routeur sous le préfixe /boitesapp.use("/boites", boitesRouter);// Aucune route précédente n'a réponduapp.use((req, res) => { res.status(404).json({ error: "Route introuvable." });});// Centralise les erreurs; les quatre paramètres sont nécessairesapp.use((err, req, res, next) => { if (res.headersSent) { return next(err); } console.error(err); const status = Number.isInteger(err.status) ? err.status : 500; const message = status < 500 ? "La requête est invalide." : "Erreur interne du serveur."; res.status(status).json({ error: message });});export default app;C’est app.use("/boites", boitesRouter) qui donne leur préfixe aux routes du module. Le router.get("/:id") répond donc à GET /boites/:id. On peut monter d’autres routeurs (/livres, /utilisateurs) de la même façon, chacun dans son fichier.
Les deux derniers middlewares s’exécutent seulement si les routes précédentes n’ont pas terminé la réponse ou si une erreur leur est transmise. Le gestionnaire d’erreurs se reconnaît à ses quatre paramètres : (err, req, res, next).
Le démarrage : index.js
Section intitulée « Le démarrage : index.js »import app from "./src/app.js";app.listen(3000, () => { console.log("Serveur démarré sur http://localhost:3000");});Le point d’entrée du projet ne fait plus qu’une chose : démarrer le serveur.
Ce qu’on a construit
Section intitulée « Ce qu’on a construit »On est parti d’un serveur minimal et on a abouti à une petite API structurée en mémoire :
- un serveur minimal (
app.get,res.send,app.listen) ; - des réponses JSON (
res.json) ; - des paramètres de route (
:id,req.params) avec gestion du404; - des paramètres de requête (
?quartier=…,req.query) pour filtrer une liste ; - la lecture de corps POST (
express.json,req.body) avec201et400; - la modification et la suppression (
PUT,DELETE) avec200et204, ce qui complète le CRUD ; - une décomposition en modules (routeur, données, application et démarrage) où chaque fichier a une responsabilité claire ;
- des réponses JSON cohérentes pour les routes inconnues et les erreurs inattendues.
Cette structure modulaire donne des points d’extension clairs pour ajouter ensuite une base de données, de l’authentification ou d’autres ressources. C’est le squelette qu’on étendra pour le projet.