IFT3225

Qu’est-ce qu’Express ?

Express est un cadriciel web minimaliste et non-opinionné (unopinionated) pour Node.js.

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.

GET / — serveur minimal
Client
GET
Serveur
RequeteGET/
Traitement serveur
  1. Express recoit la requete
  2. La route GET / correspond
  3. res.send() renvoie le texte
Reponse
Lance la requete pour voir la reponse.

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 :

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 :

GET /boites — repondre en JSON
Client
GET
Serveur
RequeteGET/boites
Traitement serveur
  1. La route GET /boites correspond
  2. Lire le tableau des boites base
  3. res.json() serialise et renvoie
Reponse
Lance la requete pour voir la reponse.

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), puis id=99 (n’existe pas):

GET /boites/:id — parametre de route
Client
GET
Serveur
RequeteGET/boites/14
Traitement serveur
  1. Extraire req.params.id
  2. Chercher la boite correspondante base
  3. Renvoyer la boite ou un 404
Reponse
Lance la requete pour voir la reponse.

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 :


É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) :

GET /boites?quartier=… — filtrer
Client
GET
Serveur
RequeteGET/boites?quartier=Plateau
Traitement serveur
  1. Lire req.query.quartier
  2. Si absent, tout renvoyer
  3. Sinon filtrer le tableau base
  4. res.json() renvoie le sous-ensemble
Reponse
Lance la requete pour voir la reponse.

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 :


É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 nom pour voir l’erreur 400) :

POST /boites — lire le corps
Client
GET
Serveur
RequeteGET/boites
Traitement serveur
  1. La route GET /boites correspond
  2. Lire le tableau des boites base
  3. res.json() renvoie
Reponse
Lance la requete pour voir la reponse.

Après un POST réussi, relancez le GET /boites dans 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 :

POST /boites — lire le corps
Client
GET
Serveur
RequeteGET/boites
Traitement serveur
  1. La route GET /boites correspond
  2. Lire le tableau des boites base
  3. res.json() renvoie
Reponse
Lance la requete pour voir la reponse.

Le code ajoute les deux gestionnaires.

// 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 :

Avec GET, POST, PUT et DELETE, on couvre les quatre opérations du CRUDCreate, 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 :

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.