Qu’est-ce qu’Express ?
Express est un cadriciel web minimaliste et non-opinionné (unopinionated) pour Node.js.
- Minimaliste? Express ajoute une couche mince au module
httpnatif de Node. Il gère le routage (associer une URL à du code), les middlewares (traitements en chaîne), et facilite l’écriture des réponses. Rien de plus. - Non-opinionné? Express n’impose aucune structure de projet, aucun ORM, aucun moteur de gabarits, aucune façon « officielle » d’organiser le code. Express vous donne les outils pour router et répondre ; comment structurer le reste, c’est votre décision.
Express dans son écosystème
Express n’est pas seul. Au sein de Node.js, plusieurs cadriciels du même genre existent, mais épousent des philosophies différentes :
| Cadre | Caractère | Quand le choisir |
|---|---|---|
| Express | Minimaliste, non-opinionné, omniprésent | Standard de l’industrie, immense écosystème de middlewares |
| Fastify | Minimaliste mais axé performance | Quand le débit et la validation par schéma comptent |
| Koa | Minimaliste, async/await pur | Quand on veut composer soi-même sa pile de middlewares |
| NestJS | Très opinionné, architecture imposée | Grosses équipes, applications d’entreprise structurées |
Chaque écosystème a ses cadriciels web, du minimaliste à l’opinionné :
| Univers | Minimaliste / micro | Complet / opinionné |
|---|---|---|
| JavaScript | Express, Fastify, Koa | NestJS |
| Python | Flask, FastAPI | Django |
| Java | Javalin, Spark | Spring Boot |
| Ruby | Sinatra | Rails |
Les concepts qu’on apprend ici — routes, paramètres, middlewares, codes de statut — se retrouvent dans tous ces cadriciel..
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, et les mettre à jour.
On y arrive par étapes, en partant de presque rien. À chaque étape : d’abord on simule la requête qu’on veut gérer, ensuite on écrit le code qui la gère. Voir l’échange avant de l’implémenter aide à garder en tête ce qu’on cherche à produire. À la fin, on aura une API complète (lecture, création, mise à jour, suppression) découpée proprement en modules.
Installation
Dans un dossier de projet initialisé (npm init -y avec "type": "module" dans le package.json) :
npm install express
É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 :
// index.js
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 quatre lignes sont le squelette de tout serveur Express ; tout ce qui suit ne fait qu’ajouter des routes et du traitement.
Étape 2 — Répondre en JSON
Une API ne renvoie pas du texte brut mais des données structurées. On veut maintenant 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() :
// index.js
import express from "express";
const app = express();
// Nos données, en mémoire pour l'instant
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" },
];
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 les examine dans l’ordre et exécute la première qui correspond à la requête.
É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.
Gérer le cas « introuvable » fait partie de la conception; ce n’est pas d’un détail optionnel.
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 arrivent toujours comme chaînes —
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
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 la query. Express expose ces paramètres dans req.query :
app.get("/boites", (req, res) => {
const { quartier } = req.query; // ?quartier=Plateau
// 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.- Les paramètres de requête doivent toujours être optionnels : une requête sans
?devrait être parfaitement valide. D’où leif (!quartier)qui renvoie la liste complète. Un filtre absent ne doit jamais être une erreur. - Comme les paramètres de route, ils arrivent toujours comme chaînes de caractères. Un même endpoint peut servir plusieurs besoins selon sa query : tout lister, filtrer par quartier, plus tard trier ou paginer — sans multiplier les routes.
É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 lit pas le corps tout seul — il faut un middleware pour ça. Simulez la création (et 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 routes
app.use(express.json());
app.post("/boites", (req, res) => {
const { nom, quartier } = req.body; // disponible grâce à express.json()
if (!nom) {
return res.status(400).json({ error: "Le champ 'nom' est requis." });
}
const nouvelle = { id: boites.length + 1, nom, quartier: quartier ?? "inconnu" };
boites.push(nouvelle);
res.status(201).json(nouvelle); // 201 Created
});
Le statut 201 Created (plutôt que 200) signale qu’une ressource a été créée. Ces nuances de code de statut font qu’une API parle clairement à ses clients.
É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:idDELETEretire l’élément et répond204:
// Remplacer une boîte existante
app.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 (!nom) {
return res.status(400).json({ error: "Le champ 'nom' est requis." });
}
boite.nom = nom;
boite.quartier = quartier ?? "inconnu";
res.json(boite); // 200 avec la version à jour
});
// Supprimer une boîte
app.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, le cœur de toute API REST.
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.json
L’idée directrice : chaque fichier a une seule raison de changer.
Le routeur — src/routes/boites.js
Express fournit un objet Router : un mini-app qui regroupe des routes liées, qu’on branche ensuite sur l’app principale.
// src/routes/boites.js
import { Router } from "express";
import { boites } from "../data/boites.js";
const router = Router();
router.get("/", (req, res) => {
res.json(boites);
});
router.get("/: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 });
}
res.json(boite);
});
router.post("/", (req, res) => {
const { nom, quartier } = req.body;
if (!nom) {
return res.status(400).json({ error: "Le champ 'nom' est requis." });
}
const nouvelle = { id: boites.length + 1, nom, quartier: quartier ?? "inconnu" };
boites.push(nouvelle);
res.status(201).json(nouvelle);
});
router.put("/: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 (!nom) {
return res.status(400).json({ error: "Le champ 'nom' est requis." });
}
boite.nom = nom;
boite.quartier = quartier ?? "inconnu";
res.json(boite);
});
router.delete("/: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();
});
export default router;
Les données — src/data/boites.js
// 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 dans leur module prépare le terrain : quand on branchera une vraie base de données (MongoDB), seul ce fichier changera. Les routes, elles, ne bougeront pas.
L’assemblage — src/app.js
// 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 /boites
app.use("/boites", boitesRouter);
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.
Le démarrage — index.js
// 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
On est parti de quatre lignes et on a abouti à une petite API structurée, prête à grandir :
- 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) avec200,204— le CRUD complet ; - une décomposition en modules (routeur, données, app, démarrage) où chaque fichier a une seule responsabilité.
La structure modulaire n’est pas une décoration — c’est ce qui permet d’ajouter une base de données ou de l’authentification sans tout réécrire. C’est le squelette qu’on étendra pour le projet.