backend / nodejs
Serveur HTTP natif
Explication
Ce que vous allez apprendre
- Créer un serveur HTTP avec le module natif
node:http, sans framework - Comprendre pourquoi le body d'une requête POST doit être lu manuellement
- Écrire un routeur minimal maison pour comprendre ce qu'Express automatise
- Configurer des timeouts serveur pour se protéger d'attaques basiques (Slowloris)
- Distinguer ce qu'un framework ajoute par rapport au module
httpseul
Dans quel contexte ?
Un développeur qui utilise Express depuis des mois se demande pourquoi req.body fonctionne "comme par magie" après avoir monté express.json(). Il décide de reconstruire, sans aucun framework, un mini-serveur capable de router deux routes et de lire un JSON envoyé en POST. Cette leçon suit exactement cette démarche : comprendre le module http natif rend Express beaucoup moins opaque une fois qu'on l'utilisera dans la leçon suivante.
Pourquoi voir ça avant Express
La prochaine leçon présente Express, le framework le plus utilisé avec Node. Mais avant de l'utiliser, il est très formateur de voir ce qui se passe SANS lui, avec le module natif http.
Beaucoup de développeurs utilisent un framework sans jamais comprendre ce qu'il automatise réellement. Cette leçon lève le voile sur cette "magie".
Commençons par la structure la plus simple : le serveur HTTP est un callback. createServer prend une fonction appelée à chaque requête entrante, avec deux objets : req (la requête) et res (la réponse).
Sans framework, un travail précis doit être écrit entièrement à la main : le routage. Décider quelle fonction exécuter selon l'URL et la méthode HTTP n'est fait par personne d'autre que toi, comme le montre l'exemple du "routeur maison" avec une Map.
Une surprise attend beaucoup de débutants ici : lire le corps d'une requête. Contrairement à ce qu'on pourrait attendre, le module http natif ne parse PAS automatiquement le corps d'une requête POST en JSON.
Il faut donc le faire soi-même, étape par étape. On écoute l'événement data à chaque morceau de données reçu, puis end quand la requête est entièrement reçue, avant de parser soi-même la chaîne accumulée.
| Tâche | Module http natif | Avec Express |
|---|---|---|
| Routage par URL/méthode | À écrire à la main (Map, if) | app.get(), app.post()... |
| Parsing du body JSON | À écrire à la main (data/end) | express.json() |
| Gestion d'erreurs centralisée | À écrire à la main | Middleware d'erreur dédié |
Prérequis
Cette leçon suppose que tu es à l'aise avec les streams vus précédemment : req est lui-même un stream lisible, c'est pour ça qu'on écoute ses événements data et end.
C'est exactement ce qu'Express fait automatiquement via express.json(). Comprendre cette mécanique manuelle rend express.json() beaucoup moins "magique" une fois qu'on l'utilisera.
Une fois ces bases posées, il reste un point de sécurité souvent oublié. Un serveur HTTP exposé sans configuration de timeouts est vulnérable à des attaques simples comme Slowloris, où un attaquant envoie des headers très lentement pour épuiser les connexions disponibles.
Configurer headersTimeout et requestTimeout est une protection de base. Elle reste utile indépendamment de tout framework utilisé par-dessus ce serveur natif.
Piège fréquent
Un serveur http.createServer déployé sans configurer headersTimeout accepte des connexions qui envoient leurs headers très lentement, un octet à la fois, épuisant progressivement les connexions disponibles (attaque Slowloris). Configurer ce timeout dès la création du serveur est une protection de base, quel que soit le framework utilisé par-dessus.
Et la suite ? Comprendre ces fondations permet de mieux apprécier — et de mieux déboguer — ce qu'Express fera pour toi dès la prochaine leçon : routage, parsing automatique du body, gestion d'erreurs centralisée.
Commandes & code
Serveur HTTP natif
// server.js — serveur HTTP sans framework, avec le module natif "http"
import { createServer } from "node:http";
import { URL } from "node:url";
const server = createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (req.method === "GET" && url.pathname === "/") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Bienvenue" }));
return;
}
if (req.method === "GET" && url.pathname === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Route introuvable" }));
});
server.listen(3000, () => console.log("Serveur sur http://localhost:3000"));// Lire le body d'une requête POST — le module http natif ne le parse pas automatiquement
import { createServer } from "node:http";
function readBody(req) {
return new Promise((resolve, reject) => {
let data = "";
req.on("data", (chunk) => (data += chunk));
req.on("end", () => resolve(data));
req.on("error", reject);
});
}
const server = createServer(async (req, res) => {
if (req.method === "POST" && req.url === "/echo") {
try {
const rawBody = await readBody(req);
const parsed = JSON.parse(rawBody);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ received: parsed }));
} catch {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "JSON invalide" }));
}
return;
}
res.writeHead(404);
res.end();
});
server.listen(3000);// Routeur minimal maison — comprendre ce qu'Express fait sous le capot
const routes = new Map();
function get(path, handler) {
routes.set(`GET ${path}`, handler);
}
function post(path, handler) {
routes.set(`POST ${path}`, handler);
}
get("/users", (req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify([{ id: 1, name: "Alice" }]));
});
const server = createServer((req, res) => {
const key = `${req.method} ${req.url}`;
const handler = routes.get(key);
if (handler) return handler(req, res);
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not found" }));
});// Timeouts et limites de sécurité de base sur un serveur brut
const server = createServer((req, res) => {
res.end("ok");
});
server.timeout = 30_000; // ferme la connexion après 30s d'inactivité
server.headersTimeout = 10_000; // délai max pour recevoir les headers (protection Slowloris)
server.maxHeadersCount = 100; // limite le nombre de headers acceptés
server.requestTimeout = 60_000; // délai max pour recevoir la requête complète
server.listen(3000);Résumé
http.createServerfournit unreq(stream lisible) et unres(stream inscriptible).- Le body d'une requête doit être lu manuellement via les événements
data/enddu streamreq. - Un framework comme Express automatise routing, parsing de body et gestion d'erreurs vus ici "à la main".
- Configurer les timeouts serveur (
headersTimeout,requestTimeout) protège contre certaines attaques DoS basiques.
Exercices pratiques
Mission : reconstruire un mini-serveur avant de comprendre Express
Objectif : Écrire un serveur HTTP natif capable de lire un body JSON et de router deux routes à la main, puis le protéger contre une attaque Slowloris basique.
Contexte
Avant d'introduire Express à l'équipe junior, le lead technique demande de recréer à la main, avec uniquement node:http, un serveur capable de router GET /health et POST /echo (qui renvoie le JSON reçu), sans aucune dépendance externe. Un test de charge basique révèle ensuite que le serveur reste vulnérable si des headers sont envoyés très lentement, un octet à la fois.