Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 321 minIntermédiaire

Express.js : créer un serveur et des routes

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • créer un serveur Express 5 et le tester avec curl ;
  • écrire des routes et lire paramètres, chaîne de requête et corps JSON ;
  • répondre avec le bon code de statut et les bons en-têtes ;
  • ranger les routes d'une ressource dans un Router ;
  • éviter les pièges de la syntaxe des chemins d'Express 5.

4 exercices corrigés

Express est le framework web le plus utilisé avec Node.js : une bibliothèque minimaliste qui reçoit les requêtes HTTP, les oriente vers la bonne fonction selon la méthode et l'adresse (le routage), et t'aide à construire la réponse. Il ne t'impose ni base de données ni structure de dossiers : c'est toi qui organises le reste. Ce chapitre crée un premier serveur Express 5, montre comment lire une requête et y répondre, range les routes dans un Router et liste ce qui a changé avec Express 5.

Pourquoi un framework ? Le même serveur avec et sans Express

Node.js sait créer un serveur HTTP tout seul, avec le module node:http :

js
import { createServer } from 'node:http';

const serveur = createServer((req, res) => {
  if (req.method === 'GET' && req.url === '/sante') {
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ statut: 'ok' }));
  } else {
    res.writeHead(404);
    res.end();
  }
});

serveur.listen(3000);

Ça fonctionne, mais tout est à ta charge : comparer les méthodes et les adresses à la main, extraire 12 de /v1/fiches/12, découper la chaîne de requête, lire et analyser le corps JSON morceau par morceau, écrire les en-têtes. Avec vingt routes, le fichier devient illisible. Express apporte :

  • un routage déclaratif : app.get('/v1/fiches/:id', …) ;
  • des objets req et res enrichis : req.params, req.query, req.body, res.status(), res.json() ;
  • les middlewares, des fonctions qui s'enchaînent sur chaque requête (journal, sécurité, authentification) : c'est le sujet du chapitre 4 ;
  • un écosystème de middlewares prêts à l'emploi : CORS, en-têtes de sécurité, limitation de débit, téléversement.

D'autres frameworks Node.js existent : Fastify (rapide, validation par schéma intégrée), NestJS (structuré à la manière d'Angular ou de Spring, en TypeScript), Hono (léger, tourne aussi hors de Node.js). Express reste le plus répandu, le plus documenté et celui qu'on retrouve dans les sujets et les stages : c'est le bon choix pour apprendre.

Le premier serveur Express

bash
npm install express
js
// src/server.js : première version, tout dans un fichier
import express from 'express';

const app = express();
app.use(express.json()); // lit les corps JSON et remplit req.body

app.get('/sante', (req, res) => {
  res.json({ statut: 'ok' });
});

const port = Number(process.env.PORT ?? 3000);
app.listen(port, (erreur) => {
  if (erreur) throw erreur;
  console.log(`API à l'écoute sur http://localhost:${port}`);
});
bash
node --watch src/server.js
text
API à l'écoute sur http://localhost:3000
  • express() crée l'application, l'objet sur lequel on déclare tout ;
  • app.get(chemin, gestionnaire) déclare une route : quand une requête GET arrive sur /sante, Express appelle le gestionnaire avec la requête req et la réponse res ;
  • app.listen(port, rappel) démarre le serveur HTTP. Avec Express 5, une erreur au démarrage (le port est déjà pris, EADDRINUSE) arrive dans le paramètre erreur du rappel au lieu d'être levée ailleurs.

Le serveur tourne en continu : le terminal reste occupé tant qu'il écoute. Ctrl+C l'arrête.

Tester avec le navigateur, curl ou Bruno

Le navigateur n'envoie que des GET quand tu tapes une adresse : suffisant pour /sante, pas pour créer une fiche. Pour le reste, utilise curl en ligne de commande ou un client graphique comme Bruno ou Postman (le cours API REST les présente en détail).

bash
curl -i http://localhost:3000/sante
http
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 15
ETag: W/"f-j2s+TDm4JXnuck/KIdixU5aNt9c"
Date: Mon, 02 Nov 2026 14:21:31 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"statut":"ok"}

L'option -i affiche les en-têtes de la réponse. Express a ajouté tout seul Content-Type (parce que tu as appelé res.json), Content-Length, un ETag (une empreinte de la réponse, utile au cache) et X-Powered-By: Express, un en-tête qui annonce ta technologie à tout le monde : on le supprimera au chapitre 10.

Les routes : une méthode et un chemin

Une route associe une méthode HTTP et un chemin à une fonction. Express a une méthode par verbe HTTP :

js
app.get('/v1/fiches', lister);          // lire la collection
app.post('/v1/fiches', creer);          // créer
app.get('/v1/fiches/:id', lire);        // lire un élément
app.patch('/v1/fiches/:id', modifier);  // modifier en partie
app.put('/v1/fiches/:id/lignes-forfait', remplacerLignes); // remplacer
app.delete('/v1/fiches/:id', supprimer); // supprimer
app.all('/v1/maintenance', fermer);     // toutes les méthodes

Le choix des méthodes, des adresses et des codes de statut relève de la conception de l'API, traitée dans le cours API REST : ici, on traduit ces choix en code.

Comment Express choisit-il la route ?

  • il parcourt les routes dans l'ordre où tu les as déclarées et s'arrête à la première qui correspond. Une route /v1/fiches/:id déclarée avant /v1/fiches/stats attrape /v1/fiches/stats avec id = "stats" : déclare les chemins fixes avant les chemins à paramètre ;
  • par défaut, la comparaison ignore la casse (/v1/Fiches = /v1/fiches) et la barre oblique finale (/v1/fiches/ = /v1/fiches) ;
  • si aucune route ne correspond, Express répond 404 avec une petite page HTML « Cannot GET /v1/frais ». Pour une API, on la remplace par une réponse JSON (chapitre 4).

Lire la requête : paramètres, chaîne de requête, corps

Voici trois routes sur des fiches gardées en mémoire, le temps de découvrir req :

js
const fiches = [
  { id: 11, visiteurId: 7, mois: '2026-09', etat: 'VA' },
  { id: 12, visiteurId: 7, mois: '2026-10', etat: 'CR' },
  { id: 13, visiteurId: 8, mois: '2026-10', etat: 'CL' },
];

// GET /v1/fiches?etat=CL
app.get('/v1/fiches', (req, res) => {
  const { etat } = req.query;
  res.json(etat ? fiches.filter((f) => f.etat === etat) : fiches);
});

// GET /v1/fiches/12
app.get('/v1/fiches/:id', (req, res) => {
  const fiche = fiches.find((f) => f.id === Number(req.params.id));
  if (!fiche) {
    return res.status(404).json({ erreur: `La fiche ${req.params.id} n'existe pas.` });
  }
  res.json(fiche);
});

// POST /v1/fiches  {"visiteurId": 7, "mois": "2026-11"}
app.post('/v1/fiches', (req, res) => {
  const { visiteurId, mois } = req.body ?? {};
  if (!visiteurId || !mois) {
    return res.status(422).json({ erreur: 'visiteurId et mois sont obligatoires.' });
  }
  const fiche = { id: Math.max(...fiches.map((f) => f.id)) + 1, visiteurId, mois, etat: 'CR' };
  fiches.push(fiche);
  res.status(201).location(`/v1/fiches/${fiche.id}`).json(fiche);
});
SourceExemple d'URL ou de requêteDans le codeValeur
Paramètre de chemin/v1/fiches/12 sur la route /v1/fiches/:idreq.params.id"12"
Chaîne de requête/v1/fiches?etat=CL&page=2req.query.etat, req.query.page"CL", "2"
Corps JSON{"visiteurId": 7, "mois": "2026-11"}req.body.visiteurId7
En-têteAuthorization: Bearer eyJ…req.get('Authorization')"Bearer eyJ…"
Adresse du clientreq.ip"::1" en local

Les pièges :

  • les paramètres et la chaîne de requête sont toujours des chaînes. req.params.id === 12 est faux : convertis avec Number(…) et vérifie le résultat ;
  • un paramètre de requête répété devient un tableau : ?etat=CL&etat=VA donne req.query.etat = ['CL', 'VA']. Ne suppose jamais qu'une valeur reçue a le type attendu : le chapitre 8 valide tout avec Zod ;
  • req.body n'existe que si un middleware a lu le corps. express.json() le fait pour les requêtes envoyées avec Content-Type: application/json. Sans cet en-tête, ou sans le middleware, req.body vaut undefined en Express 5, d'où le ?? {} ci-dessus ;
  • req.query est en lecture seule en Express 5 : req.query.page = 1 est ignoré sans erreur, et req.query = {…} lève une TypeError. Range les valeurs converties ailleurs (le chapitre 8 utilise req.valide).
bash
curl -i -X POST http://localhost:3000/v1/fiches \
  -H 'Content-Type: application/json' \
  -d '{"visiteurId": 7, "mois": "2026-11"}'
http
HTTP/1.1 201 Created
Location: /v1/fiches/14
Content-Type: application/json; charset=utf-8

{"id":14,"visiteurId":7,"mois":"2026-11","etat":"CR"}

La même commande sans -H 'Content-Type: application/json' envoie le corps comme un formulaire (application/x-www-form-urlencoded) : express.json() l'ignore, req.body reste undefined, et l'API répond 422. C'est l'erreur la plus fréquente quand on teste une API à la main.

Répondre : code de statut, JSON et en-têtes

Méthode de resEffetExemple
res.status(code)Fixe le code de statut (200 par défaut) ; se chaîneres.status(201)
res.json(objet)Envoie l'objet en JSON avec le bon Content-Typeres.json(fiche)
res.sendStatus(code)Envoie un code sans corps utileres.sendStatus(204)
res.location(url)Ajoute l'en-tête Locationres.location('/v1/fiches/14')
res.set(nom, valeur)Ajoute un en-têteres.set('Cache-Control', 'no-store')
res.type(type)Fixe le Content-Typeres.type('application/problem+json')
res.send(texte)Envoie du texte ou un Bufferres.send('ok')
res.redirect(code, url)Redirige (l'ordre des arguments a changé en Express 5)res.redirect(301, '/v1/fiches')
res.download(chemin)Envoie un fichier à téléchargerres.download(cheminPdf)

Une requête reçoit une seule réponse. Si ton code en envoie deux, Express lève ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client. La cause habituelle est un return oublié :

js
app.get('/v1/fiches/:id', (req, res) => {
  if (req.params.id === '99') {
    res.status(404).json({ erreur: 'introuvable' }); // oubli du return : le code continue
  }
  res.json({ id: Number(req.params.id) });           // deuxième réponse : erreur
});

Écris return res.status(404).json(…), ou mieux, lève une erreur qu'un gestionnaire central transformera en réponse (chapitre 4).

Ranger les routes avec express.Router

Quand les routes se multiplient, on les regroupe par ressource dans un Router, un mini-routeur qu'on branche ensuite sur un préfixe :

js
// src/routes/fiches.routes.js
import { Router } from 'express';
import * as fiches from '../controllers/fiches.controller.js';

export const routeurFiches = Router();

routeurFiches.get('/', fiches.lister);        // GET /v1/fiches
routeurFiches.post('/', fiches.creer);        // POST /v1/fiches
routeurFiches.get('/:id', fiches.lire);       // GET /v1/fiches/12
routeurFiches.patch('/:id', fiches.changerEtat);
routeurFiches.delete('/:id', fiches.supprimer);
js
// src/app.js
import { routeurFiches } from './routes/fiches.routes.js';

app.use('/v1/fiches', routeurFiches);

Dans le routeur, les chemins sont relatifs au préfixe : '/:id' répond à /v1/fiches/12. Le préfixe /v1 porte la version de l'API (versionner une API). Le chapitre 5 sépare ensuite les routes, les contrôleurs et l'accès aux données.

Servir des fichiers statiques

express.static sert le contenu d'un dossier tel quel : une page d'accueil, des images, le front compilé.

js
app.use(express.static('public')); // public/index.html répond à GET /

En Express 5, les fichiers et dossiers dont le nom commence par un point (.env, .git) sont ignorés par défaut et répondent 404 : une bonne protection. Ne place jamais un dossier de fichiers envoyés par les utilisateurs dans un dossier statique public (chapitre 8).

Ce qui a changé avec Express 5

Express 5.0 est sorti en septembre 2024, dix ans après Express 4 ; c'est aujourd'hui la version installée par défaut (5.3 en novembre 2026). Beaucoup de tutoriels, et le code généré par d'anciennes IA, sont encore écrits pour Express 4. Les différences qui comptent :

SujetExpress 4Express 5
Erreur dans une fonction asyncRequête bloquée et promesse rejetée non gérée, sauf try/catch + next(err)Transmise automatiquement au gestionnaire d'erreurs (chapitre 4)
Joker dans un chemin'/fichiers/*'Joker nommé : '/fichiers/*chemin' ; '/{*chemin}' accepte aussi la racine
Paramètre facultatif'/rapports/:annee?'Accolades : '/rapports{/:annee}'
Expressions régulières dans une chaîne de chemin ('/fiches/:id(\\d+)')AcceptéesRefusées ; caractères ( ) [ ] ? + ! réservés (un objet RegExp reste accepté)
req.queryModifiable ; analyse « étendue » (?filtre[etat]=CL donne un objet)Lecture seule ; analyse simple (filtre[etat] reste une clé)
req.body sans middleware{}undefined
app.listen qui échoueErreur levée sur le serveurErreur passée au rappel
Anciennes écrituresres.send(201) (envoie le code), res.json(obj, 201), req.param(), app.del(), res.redirect('back')res.sendStatus(201), res.status(201).json(obj), req.params, app.delete() ; res.send(201) envoie désormais le texte « 201 »
res.status(…)Accepte n'importe quoiLève une erreur hors de 100 à 999

Ces anciennes syntaxes de chemin ne sont pas ignorées : elles plantent au démarrage. app.get('/ancien/*', …) lève TypeError: Missing parameter name at index 9: /ancien/*, et '/ancien/:id?' lève TypeError: Unexpected ? at index 11: /ancien/:id?, suivis d'un lien vers la documentation de path-to-regexp.

js
// Express 5 : le joker nommé reçoit un tableau de segments
app.get('/fichiers/*chemin', (req, res) => {
  res.json({ chemin: req.params.chemin }); // GET /fichiers/2026/10/ticket.pdf → ["2026","10","ticket.pdf"]
});

// Paramètre facultatif entre accolades
app.get('/rapports{/:annee}', (req, res) => {
  res.json({ annee: req.params.annee ?? null }); // /rapports → null, /rapports/2026 → "2026"
});

Séparer l'application et le serveur

Dernière étape d'organisation : on sépare la construction de l'application (app.js) de son démarrage (server.js). Les tests du chapitre 12 importent app sans ouvrir de port.

js
// src/app.js
import express from 'express';
import { routeurFiches } from './routes/fiches.routes.js';

export const app = express();
app.use(express.json());
app.get('/sante', (req, res) => res.json({ statut: 'ok' }));
app.use('/v1/fiches', routeurFiches);
js
// src/server.js
import { app } from './app.js';
import { config } from './config.js';

app.listen(config.port, (erreur) => {
  if (erreur) throw erreur;
  console.log(`API AppliFrais à l'écoute sur http://localhost:${config.port}`);
});

L'arborescence du projet à ce stade :

text
applifrais-api/
├── package.json
├── package-lock.json
├── .env               (non versionné)
├── .env.example
└── src/
    ├── server.js      démarre le serveur
    ├── app.js         construit l'application
    ├── config.js      lit les variables d'environnement
    ├── routes/
    │   └── fiches.routes.js
    └── controllers/
        └── fiches.controller.js

Exercices

Exercice 1 sur 4 Quelle route répond ? Réussi À revoir

js
app.get('/v1/fiches/:id', lire);
app.get('/v1/fiches/stats', statistiques);
app.get('/v1/fiches', lister);
app.post('/v1/fiches', creer);

Pour chaque requête, dis quelle fonction est appelée et avec quelle valeur de req.params.id : GET /v1/fiches, GET /v1/fiches/12, GET /v1/fiches/stats, GET /v1/Fiches/12/, DELETE /v1/fiches/12. Quelle erreur de conception repères-tu ?

Voir la réponseMasquer la réponse de l'exercice 1
  • GET /v1/fiches : lister.
  • GET /v1/fiches/12 : lire, avec req.params.id = "12" (une chaîne).
  • GET /v1/fiches/stats : lire, avec req.params.id = "stats". La route statistiques n'est jamais atteinte, car /:id est déclarée avant et correspond aussi.
  • GET /v1/Fiches/12/ : lire avec "12" ; la casse et la barre finale sont ignorées par défaut.
  • DELETE /v1/fiches/12 : aucune route ; Express répond 404 (« Cannot DELETE /v1/fiches/12 »).

Correction : déclarer /v1/fiches/stats avant /v1/fiches/:id.

Tu avais trouvé ?

Exercice 2 sur 4 Lire req Réussi À revoir

Une requête POST /v1/fiches/12/frais-hors-forfait?brouillon=true&tag=a&tag=b arrive avec l'en-tête Content-Type: application/json et le corps {"libelle":"Péage","montant":"12.20"}, sur la route routeurFiches.post('/:id/frais-hors-forfait', …) branchée sur /v1/fiches, avec express.json(). Que valent req.params.id, req.query.brouillon, req.query.tag, req.body.montant et typeof req.body.montant ? Quel problème pose le montant ?

Voir la réponseMasquer la réponse de l'exercice 2
  • req.params.id : "12" ;
  • req.query.brouillon : "true", une chaîne, pas le booléen true ;
  • req.query.tag : ["a", "b"], un tableau parce que le paramètre est répété ;
  • req.body.montant : "12.20" et typeof donne "string".

Le client a envoyé le montant entre guillemets : c'est une chaîne. Un calcul comme req.body.montant + 5 donnerait "12.205" (concaténation). L'API doit valider le type et refuser la requête avec 422 plutôt que de deviner (chapitre 8).

Tu avais trouvé ?

Exercice 3 sur 4 Corriger la réponse Réussi À revoir

Cette route doit supprimer un frais hors forfait. Corrige ses trois défauts de réponse.

js
app.delete('/v1/frais-hors-forfait/:id', (req, res) => {
  const index = frais.findIndex((f) => f.id === Number(req.params.id));
  if (index === -1) {
    res.json({ message: 'Frais introuvable' });
  }
  frais.splice(index, 1);
  res.send(200);
});
Voir la réponseMasquer la réponse de l'exercice 3
js
app.delete('/v1/frais-hors-forfait/:id', (req, res) => {
  const index = frais.findIndex((f) => f.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ erreur: 'Frais introuvable' });
  }
  frais.splice(index, 1);
  res.sendStatus(204);
});
  1. Le cas « introuvable » répondait 200 : il faut 404.
  2. Il manquait le return : le code continuait, splice(-1, 1) supprimait le dernier frais du tableau, puis une deuxième réponse provoquait ERR_HTTP_HEADERS_SENT.
  3. En Express 5, res.send(200) n'envoie plus un code de statut : il envoie le texte 200 comme corps de la réponse. Une suppression réussie répond 204 No Content, sans corps, avec res.sendStatus(204).

Tu avais trouvé ?

Exercice 4 sur 4 Migrer vers Express 5 Réussi À revoir

Un tutoriel écrit pour Express 4 contient ces lignes. Réécris-les pour Express 5.

js
app.get('/docs/*', servirDoc);
app.get('/rapports/:annee?', rapport);
app.post('/v1/fiches', (req, res) => { res.json(fiche, 201); });
app.use((req, res, next) => { req.query.page = req.query.page || 1; next(); });
Voir la réponseMasquer la réponse de l'exercice 4
js
app.get('/docs/*chemin', servirDoc);           // joker nommé ; req.params.chemin est un tableau
app.get('/rapports{/:annee}', rapport);        // paramètre facultatif entre accolades
app.post('/v1/fiches', (req, res) => { res.status(201).json(fiche); });
app.use((req, res, next) => {
  req.pagination = { page: Number(req.query.page ?? 1) }; // req.query est en lecture seule
  next();
});

La dernière ligne d'origine ne plante pas, mais n'a aucun effet en Express 5 : la page modifiée disparaît. Il faut ranger la valeur ailleurs. Le chapitre 8 le fait proprement avec Zod et req.valide.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Middleware Express : next(), erreurs async et route 404Revoir le sommaire du cours