Node.js et Express
JavaScript côté serveur, modules et npm, serveur HTTP, routes Express, middleware, API REST, gestion des erreurs et passage en production. Le backend avec le langage que tu connais déjà.
À la fin du cours, tu sais
- Comprendre ce qu'est Node.js et son modèle non bloquant
- Gérer des dépendances avec npm et organiser son code en modules
- Créer un serveur et exposer des routes avec Express
- Manipuler les requêtes et les réponses HTTP (paramètres, corps, statuts)
- Comprendre et écrire des middleware
- Construire une API REST complète (les opérations CRUD)
- Gérer les erreurs proprement et préparer le passage en production
Prérequis
- Maîtriser les bases de JavaScript, dont les fonctions et l'asynchrone (async/await)
- Node.js installé (version LTS) et un terminal pour lancer les commandes
Étape 1 sur 11 : Node.js : JavaScript côté serveur
Chapitre 1
Node.js : JavaScript côté serveur
Longtemps cantonné au navigateur, JavaScript s'exécute aussi côté serveur depuis Node.js. Comprendre pourquoi, et ce que ça change.
Tu as appris JavaScript pour animer des pages dans le navigateur. Mais depuis 2009, Node.js permet d'exécuter du JavaScript en dehors du navigateur, directement sur un serveur ou ta machine. Concrètement, Node prend le moteur JavaScript de Chrome (V8) et l'emballe pour qu'il puisse lire des fichiers, écouter le réseau, dialoguer avec une base de données : tout ce qu'un serveur doit faire. Résultat : tu peux écrire ton backend dans le même langage que ton frontend. C'est ce qui a fait le succès de Node.
Un serveur, c'est un programme qui attend des requêtes (par exemple « donne-moi la liste des articles ») et renvoie des réponses. La particularité de Node est son modèle non bloquant : au lieu d'attendre bêtement qu'une opération lente se termine (lire un fichier, interroger une base), Node lance l'opération, passe à autre chose, et traite le résultat quand il arrive, grâce à une boucle d'événements. C'est ce qui lui permet de gérer des milliers de connexions simultanées sans s'écrouler.
Le modèle asynchrone, tu le connais déjà
Bonne nouvelle : ce modèle non bloquant repose sur l'asynchrone que tu as vu en JavaScript (les promesses, async/await). Une opération d'entrée/sortie renvoie une promesse, et tu l'attends avec await sans figer le reste du programme. Tout ce que tu as appris sur async/await se transpose directement à Node : c'est le même langage, le même modèle mental.
// Node fournit des modules pour parler au système
console.log("Ce code tourne côté serveur, pas dans le navigateur.");
console.log("Version de Node :", process.version);node hello.js # exécute le script avec Node
# Vérifier que Node est installé
node --version # ex. v22.x.xCôté serveur, pas de document ni de window
document, ni window, ni DOM. Ces objets appartiennent au navigateur. Node a ses propres objets globaux (process, console, les modules pour le système de fichiers et le réseau). Coder côté serveur, c'est le même langage mais un autre environnement, avec d'autres outils à disposition.Vrai ou faux ?
Node.js permet d'utiliser document.querySelector pour manipuler une page côté serveur.
Chapitre 2
Modules et npm
Un vrai projet est découpé en modules et s'appuie sur des bibliothèques externes. npm est l'outil qui gère tout ça.
Un projet Node n'est jamais un seul fichier géant : on découpe le code en modules, des fichiers qui exportent des fonctions et en importent d'autres. La syntaxe moderne est celle des modules ES : export pour exposer, import pour utiliser. C'est la même que côté frontend, donc tu la connais déjà.
export function addition(a, b) {
return a + b;
}
export function soustraction(a, b) {
return a - b;
}import { addition } from "./maths.js";
console.log(addition(3, 4)); // 7import ou require ?
const x = require("...") et module.exports. C'est le système historique (CommonJS), encore très répandu. La syntaxe moderne import/export est celle à privilégier ; pour l'activer, ajoute "type": "module" dans ton package.json. Les deux coexistent, il faut savoir reconnaître les deux.npm : le gestionnaire de paquets
Personne ne réécrit tout depuis zéro : on s'appuie sur des paquets (des bibliothèques) publiés par la communauté. npm (Node Package Manager) est l'outil qui les installe et les gère. Il s'appuie sur deux fichiers : package.json, qui décrit ton projet et liste ses dépendances, et package-lock.json, qui verrouille les versions exactes installées pour que tout le monde ait le même environnement.
npm init -y # crée un package.json de départ
npm install express # installe le paquet Express (et l'ajoute au package.json)
# Les paquets sont téléchargés dans le dossier node_modules/
# (à ne JAMAIS versionner : on le met dans .gitignore)Après un npm install express, le paquet est téléchargé dans node_modules/ et ajouté à la liste des dépendances de ton package.json. Ce dossier node_modules/ peut peser très lourd : on ne le versionne jamais avec Git (on l'ajoute au .gitignore). Quand quelqu'un récupère ton projet, un simple npm install relit le package.json et réinstalle tout à l'identique.
Vrai ou faux ?
Le dossier node_modules/ doit être versionné avec Git et poussé sur le dépôt.
Chapitre 3
Un premier serveur HTTP
Node sait créer un serveur web avec ses seuls outils, mais c'est vite fastidieux. C'est là qu'intervient Express.
Node inclut un module http pour créer un serveur web sans rien installer. C'est instructif de le voir une fois, pour comprendre ce qu'Express nous simplifiera ensuite. Un serveur écoute sur un port (un numéro, souvent 3000 en développement) et répond à chaque requête reçue.
import { createServer } from "http";
const serveur = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("Bonjour depuis Node !");
});
serveur.listen(3000, () => {
console.log("Serveur à l'écoute sur http://localhost:3000");
});Ce serveur fonctionne, mais tout est manuel : il faut gérer soi-même les en-têtes, le type de contenu, et distinguer les différentes URL à la main avec des if. Dès qu'on ajoute plusieurs routes, ce code devient un plat de spaghettis. C'est exactement le problème qu'Express résout.
Le même serveur avec Express
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send("Bonjour depuis Express !");
});
app.listen(3000, () => {
console.log("Serveur à l'écoute sur http://localhost:3000");
});Express est le framework web le plus populaire de Node : une fine couche au-dessus du module http qui rend tout plus lisible. On crée une application avec express(), on déclare des routes avec des méthodes comme app.get(), et on démarre l'écoute avec app.listen(). Le code est plus court, plus clair, et surtout il grandit proprement quand le projet se complexifie. C'est le standard pour construire une API en Node.
Relance automatique en développement
node --watch app.js relance ton serveur à chaque sauvegarde. Historiquement, on utilisait l'outil nodemon pour ça. Un petit confort qui change la vie pendant le développement.Vrai ou faux ?
Express est un langage de programmation différent de JavaScript.
Chapitre 4
Routes et méthodes HTTP
Une API répond à différentes URL, avec différentes méthodes HTTP. Express permet de déclarer tout ça de façon lisible.
Le web repose sur le protocole HTTP. Chaque requête a une méthode qui exprime une intention : GET pour lire, POST pour créer, PUT ou PATCH pour modifier, DELETE pour supprimer. Une route associe une méthode et un chemin (l'URL) à une fonction qui traite la requête. Express te laisse déclarer ces routes très naturellement.
app.get("/articles", (req, res) => {
res.send("Liste des articles");
});
app.post("/articles", (req, res) => {
res.send("Article créé");
});
app.delete("/articles/42", (req, res) => {
res.send("Article 42 supprimé");
});Chaque route reçoit deux objets essentiels : req (la requête, qui contient tout ce que le client envoie) et res (la réponse, qu'on construit pour répondre). La méthode res.send() envoie une réponse, res.json() envoie des données au format JSON, le format d'échange standard des API.
Paramètres d'URL et query
// Paramètre de route : le :id est une variable
app.get("/articles/:id", (req, res) => {
const id = req.params.id; // ex. "42" pour /articles/42
res.json({ id, titre: "Un article" });
});
// Query string : les valeurs après le ?
app.get("/recherche", (req, res) => {
const terme = req.query.q; // ex. "node" pour /recherche?q=node
res.json({ recherche: terme });
});Deux façons de recevoir des données dans une URL. Le paramètre de route (:id) capture une partie variable du chemin : pour /articles/42, req.params.id vaut "42". La query string (ce qui suit le ?) sert aux options comme un filtre ou une recherche : pour /recherche?q=node, req.query.q vaut "node". Le corps de la requête (req.body), lui, transporte les données d'un POST, on y vient au chapitre middleware.
Les méthodes HTTP portent du sens
GET ne doit jamais modifier de données (il est « sûr »), DELETE supprime, POST crée. Un jury RNCP attend que tu saches expliquer pourquoi une suppression se fait en DELETE et pas en GET. On approfondit ces conventions dans le cours Concevoir une API REST.Vrai ou faux ?
Dans la route app.get("/articles/:id"), la valeur de :id se lit avec req.params.id.
Chapitre 5
Les middleware
Le concept central d'Express : des fonctions qui s'intercalent dans le traitement d'une requête. Log, authentification, lecture du corps... tout passe par là.
Le middleware est l'idée qui structure tout Express. Un middleware est une fonction qui s'exécute entre la réception d'une requête et l'envoi de la réponse. Elle reçoit req, res et une fonction spéciale next qu'elle appelle pour passer la main au middleware suivant. On enchaîne ainsi une série de traitements : lire le corps, vérifier l'authentification, journaliser, avant d'arriver à la route finale.
// Un middleware qui journalise chaque requête
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next(); // indispensable : passe au traitement suivant
});app.use() enregistre un middleware qui s'applique à toutes les requêtes. Ce middleware affiche la méthode et l'URL, puis appelle next() pour continuer. Oublier next() est le piège classique : la requête reste bloquée, la réponse n'arrive jamais, et le client attend indéfiniment. Un middleware doit soit appeler next(), soit envoyer une réponse (res.send), jamais rien du tout.
Le middleware qui lit le corps JSON
// Sans ce middleware, req.body est undefined sur un POST
app.use(express.json());
app.post("/articles", (req, res) => {
const nouvel = req.body; // le JSON envoyé par le client
console.log(nouvel.titre);
res.status(201).json(nouvel); // 201 = "créé"
});Le middleware intégré express.json() est indispensable : il lit le corps JSON d'une requête et le range dans req.body. Sans lui, req.body vaut undefined et tu passes des heures à chercher pourquoi tes données n'arrivent pas. C'est l'oubli numéro un des débutant·es. On le place tout en haut, avant les routes, pour qu'il s'applique partout.
L'ordre compte
Les middleware s'exécutent dans l'ordre de déclaration
express.json() doit venir avant les routes qui lisent req.body. Beaucoup de bugs Express viennent simplement d'un mauvais ordre de déclaration. Quand quelque chose ne se déclenche pas, vérifie l'ordre.Vrai ou faux ?
Un middleware qui n'appelle ni next() ni une méthode de réponse laisse la requête bloquée.
Chapitre 6
Construire une API REST
En assemblant routes, méthodes HTTP et JSON, on construit une API REST : les quatre opérations de base sur une ressource.
Une API REST expose des ressources (des articles, des utilisateurs) que le client peut lire et modifier via les méthodes HTTP. Les quatre opérations fondamentales portent le nom de CRUD : Create (créer, POST), Read (lire, GET), Update (modifier, PUT/PATCH), Delete (supprimer, DELETE). Construisons une petite API de tâches, en gardant les données en mémoire dans un tableau pour rester simple.
let taches = [
{ id: 1, titre: "Apprendre Node", faite: false },
];
let prochainId = 2;
// READ : lister toutes les tâches
app.get("/taches", (req, res) => {
res.json(taches);
});
// READ : lire une tâche par son id
app.get("/taches/:id", (req, res) => {
const tache = taches.find((t) => t.id === Number(req.params.id));
if (!tache) {
return res.status(404).json({ erreur: "Tâche introuvable" });
}
res.json(tache);
});Deux détails importants ici. D'abord, req.params.id est toujours une chaîne : on le convertit en nombre avec Number() pour comparer. Ensuite, quand la tâche n'existe pas, on renvoie un statut 404 (« non trouvé ») avec un message, au lieu de laisser planter. Choisir le bon code de statut HTTP est essentiel : il indique au client si tout s'est bien passé (200), si une ressource a été créée (201), si elle est absente (404), ou s'il y a eu une erreur (500).
Créer, modifier, supprimer
// CREATE : ajouter une tâche
app.post("/taches", (req, res) => {
const nouvelle = { id: prochainId++, titre: req.body.titre, faite: false };
taches.push(nouvelle);
res.status(201).json(nouvelle); // 201 = créé
});
// UPDATE : basculer l'état d'une tâche
app.patch("/taches/:id", (req, res) => {
const tache = taches.find((t) => t.id === Number(req.params.id));
if (!tache) return res.status(404).json({ erreur: "Introuvable" });
tache.faite = req.body.faite;
res.json(tache);
});
// DELETE : supprimer une tâche
app.delete("/taches/:id", (req, res) => {
taches = taches.filter((t) => t.id !== Number(req.params.id));
res.status(204).end(); // 204 = succès, sans contenu
});Voilà une API REST complète : les quatre opérations CRUD sur la ressource taches, chacune avec sa méthode HTTP et son code de statut approprié. Le 201 signale une création, le 204 un succès sans contenu à renvoyer (typique d'une suppression). Ce squelette est exactement ce que tu construiras en entreprise, à ceci près que les données viendront d'une base de données au lieu d'un tableau en mémoire.
Teste ton API sans frontend
curl envoient des requêtes directement. Tu peux aussi tester un GET dans ton navigateur en tapant l'URL. C'est ainsi qu'on développe et vérifie une API, avant même que le frontend existe.Vrai ou faux ?
Après avoir créé une ressource avec succès via POST, le code de statut HTTP approprié est 201.
Chapitre 7
Gérer les erreurs et valider
Une API solide ne fait pas confiance au client : elle valide les données reçues et gère proprement les erreurs, sans jamais planter.
Une API va recevoir des données incomplètes, mal formées, ou carrément malveillantes. La règle d'or : ne jamais faire confiance aux données du client. On les valide avant de les utiliser, et on renvoie une erreur claire si elles ne conviennent pas. Le bon code de statut pour une donnée invalide est 400 (« mauvaise requête »).
app.post("/taches", (req, res) => {
const { titre } = req.body;
// Validation : le titre est obligatoire et non vide
if (!titre || titre.trim() === "") {
return res.status(400).json({ erreur: "Le titre est obligatoire" });
}
const nouvelle = { id: prochainId++, titre, faite: false };
taches.push(nouvelle);
res.status(201).json(nouvelle);
});Ici on vérifie que titre est bien présent et non vide avant de créer la tâche. Le return devant res.status(400) est crucial : sans lui, le code continuerait et tenterait de créer la tâche malgré l'erreur. Pour des validations plus complètes (types, longueurs, formats), on s'appuie sur des bibliothèques dédiées comme Zod ou Joi, plutôt que d'empiler les if à la main.
Gérer les erreurs asynchrones
app.get("/taches/:id", async (req, res) => {
try {
const tache = await chercherEnBase(req.params.id); // peut échouer
if (!tache) return res.status(404).json({ erreur: "Introuvable" });
res.json(tache);
} catch (erreur) {
console.error(erreur);
res.status(500).json({ erreur: "Erreur serveur" });
}
});Dès qu'une route fait une opération asynchrone (interroger une base, appeler une autre API), on l'entoure d'un try / catch. En cas d'échec imprévu, on renvoie un statut 500 (« erreur serveur ») avec un message générique, et on journalise l'erreur détaillée côté serveur avec console.error. Point de sécurité important : ne jamais renvoyer le détail technique de l'erreur au client, cela pourrait exposer des informations sensibles sur ton système.
Le middleware d'erreur centralisé
try/catch partout devient lourd. Express propose un middleware d'erreur spécial, reconnaissable à ses quatre paramètres (err, req, res, next), déclaré en dernier. Toutes les erreurs y sont canalisées, ce qui centralise leur gestion en un seul endroit. C'est le motif propre pour une vraie API, à découvrir une fois les bases acquises.🧭 Ton POST fonctionne mais req.body est toujours undefined
Tu envoies un POST avec un corps JSON depuis Postman, mais côté serveur <code>req.body</code> vaut <code>undefined</code> et ton API plante. Comment débogues-tu ?
La route app.post est bien atteinte (un console.log le confirme), mais req.body est undefined. Quelle piste explores-tu en premier ?
Pour aller plus loin
- La documentation officielle d'Express · expressjs.com (nouvel onglet)
- Node.js, la documentation d'apprentissage · nodejs.org (nouvel onglet)
- Les codes de statut HTTP expliqués (MDN) · MDN (nouvel onglet)
- Enchaîne avec : Concevoir une API REST · Ce site (nouvel onglet)
Chapitre 8
Vers la production
Passer d'un serveur qui tourne sur ta machine à une API prête pour la production demande quelques réflexes supplémentaires.
Un serveur qui marche sur ta machine n'est pas encore une application de production. Quelques éléments font la différence, et un jury comme un·e recruteur·euse les attendent. Le premier : ne jamais écrire de valeurs sensibles (mots de passe, clés d'API, adresse de la base) en dur dans le code. On les place dans des variables d'environnement, lues depuis un fichier .env qu'on ne versionne jamais.
// .env (jamais versionné, ajouté au .gitignore)
// PORT=3000
// DATABASE_URL=postgres://...
// Node lit automatiquement le .env depuis la v20 avec --env-file
const port = process.env.PORT || 3000;
app.listen(port, () => {
console.log(`Serveur démarré sur le port ${port}`);
});process.env donne accès aux variables d'environnement. On y range la configuration qui change selon l'environnement (développement, production) et tout ce qui est secret. Le || 3000 fournit une valeur de repli si la variable n'est pas définie. Ainsi, le même code tourne en local et en production, juste avec des variables différentes.
Structurer et sécuriser
- Découpe ton code : sépare les routes, la logique métier et l'accès aux données dans des fichiers distincts, plutôt qu'un seul gros
app.js. - Ajoute les en-têtes de sécurité avec le middleware
helmet(une ligne, une grosse protection). - Gère le CORS avec le middleware
corspour autoriser ton frontend à appeler l'API. - Ne renvoie jamais les détails d'erreur au client en production ; journalise-les côté serveur.
- Valide toutes les entrées : c'est ta première ligne de défense contre les attaques.
Connecter une vraie base de données
Dans nos exemples, les données vivaient dans un tableau en mémoire : elles disparaissent au redémarrage. En production, on les stocke dans une base de données (PostgreSQL, MySQL, MongoDB). Ton serveur Express s'y connecte et remplace les taches.push() par de vraies requêtes. Si tu as suivi les cours SQL, tu as déjà les bases pour écrire ces requêtes ; côté Node, un ORM comme Prisma simplifie beaucoup ce dialogue.
Tu tiens le backend, la boucle est bouclée
Vrai ou faux ?
Il est acceptable d'écrire les mots de passe et clés d'API directement dans le code, tant que le dépôt est privé.
🛠️ Exercice optionnel
Une API REST de carnet de contacts
Tu vas construire une petite API REST complète avec Express : les quatre opérations CRUD sur une ressource « contacts », le middleware de lecture JSON, la validation des entrées et les bons codes de statut. Les données restent en mémoire dans un tableau, l'objectif est de maîtriser la structure d'une API.
Ta mission
- Mise en place : une app Express,
app.use(express.json())tout en haut, un tableaucontacts(objets{ id, nom, email }) et un compteur d'id. - GET
/contacts: renvoie la liste complète en JSON. - GET
/contacts/:id: renvoie un contact, ou un404s'il n'existe pas. - POST
/contacts: valide quenometemailsont présents (400sinon), crée le contact et renvoie201avec le nouvel objet. - DELETE
/contacts/:id: supprime le contact et renvoie204. - Bonus : un middleware de log qui affiche méthode et URL de chaque requête, placé avant les routes.
Tu bloques ? Des indices, à dévoiler quand tu en as besoin.
Indice 1
Indice masqué.
Indice 2
Indice masqué.
Indice 3
Indice masqué.
✅ QCM de fin de cours
Teste tes acquis
10 questions, plusieurs réponses parfois possibles. Coche tout ce qui te semble juste, puis valide pour voir ton score et les explications.
- 1
Qu'est-ce que Node.js ?
- 2
Le dossier
node_modules/... - 3
Qu'est-ce qu'Express ?
- 4
Quelle méthode HTTP utilise-t-on pour créer une ressource ?
- 5
Comment lit-on le paramètre
:idde la route/taches/:id? - 6
Qu'est-ce qu'un middleware en Express ?
- 7
Pourquoi
req.bodyest-ilundefinedsur un POST ? - 8
Quel code de statut HTTP renvoyer quand une ressource demandée n'existe pas ?
- 9
Où doit-on stocker les secrets (mots de passe, clés d'API) ?
- 10
Que signifie l'acronyme CRUD ?
Tu peux laisser des questions sans réponse, elles compteront comme fausses.
🎓 Attestation
Ton attestation de réussite
Termine le QCM avec au moins 80% de bonnes réponses pour débloquer ton attestation de réussite.
Tu veux ce cours pour ton équipe ?
Je peux adapter et animer ce cours pour tes formateur·ices ou tes apprenant·es, en présentiel ou en distanciel. Parlons-en pendant l'audit gratuit.
Réserver un audit gratuit →