Comprendre comment fonctionne un serveur web en le construisant de zero. Pas de framework, pas de magie.
Un serveur web, c'est comme le serveur dans un restaurant.
CLIENT (toi au restaurant) SERVEUR WEB (le serveur du resto)
┌─────────────────────┐ ┌─────────────────────┐
│ │ │ │
│ "Je voudrais une" │──────────────▶ Reçoit la demande │
│ "page accueil" │ │ │
│ │ │ │
│ │◀──────────────│ Va chercher en │
│ │ Voila ! │ cuisine (fichiers) │
│ │ │ │
└─────────────────────┘ └─────────────────────┘
Ce qui se passe, en detail :
https://google.com)Tout ça se passe en quelques millisecondes. Et le serveur, bah il fait ça en boucle, toute la journée, pour des millions de personnes en même temps.
| Cas d'utilisation | Ce que fait le serveur |
|---|---|
| Afficher une page web | Envoie le fichier HTML au navigateur |
| API pour une app mobile | Envoie des donnees JSON (pas du HTML) |
| Formulaire de contact | Recoit les donnees, les traite, confirme |
| Images et videos | Envoie les fichiers statiques au navigateur |
| Jeu en ligne | Gere les connexions des joueurs en temps reel |
| Authentification | Verifie login/mdp, cree une session |
Tu vas me dire : "Mais pourquoi faire ça à la main ? Express existe déjà !"
Bonne question. Voici pourquoi :
Quand tu utilises Express, tu fais app.get('/', ...) et ça marche. Mais tu sais vraiment ce qui se passe ? Comment le serveur fait pour savoir que c'est un GET ? Comment il parse le body ? Comment il gere les headers ?
En le construisant toi-même, tu comprends chaque morceau. Et quand Express fait des trucs bizarres, tu sais pourquoi.
Si tu comprends comment fonctionne un serveur basique, tu peux debug les problèmes de Express plus facilement. "Ah oui, en fait Express fait du body parsing automatiquement, c'est pour ça que j'ai ce bug."
Tu pourras comprendre les différences entre Express, Fastify, Koa, Hono... Et choisir le bon outil pour le bon projet.
C'est comme apprendre à conduire. Tu peux prendre des cours auto-école (frameworks), mais comprendre comment fonctionne un moteur ça aide à mieux conduire.
Ce serveur "from scratch" est pour apprendre. Pas pour mettre en production sur un site à 1 million d'utilisateurs.
Avant de commencer, voici à quoi tu peux t'attendre.
| Critere | Node.js natif | Express | Fastify |
|---|---|---|---|
| Facilite | Manuel, tout gérer | Simple, beaucoup de magie | Simple, performant |
| Routing | A implementer soi-même | Integre | Integre |
| Body parsing | A implementer soi-même | Integre (body-parser) | Integre |
| Middlewares | A implementer soi-même | Ecosystème enorme | Plugin system |
| Performance | Tres bon (pas d'overhead) | Bon | Excellent |
| Apprentissage | Excellente base | Facile pour debuter | Bon compromis |
| Production ready | Non (sans beaucoup de travail) | Oui | Oui |
Debutant Savoir ce qu'est un fichier et un dossier
Debutant Savoir ouvrir un terminal (Invite de commandes sur Windows)
Debutant+ Avoir touche un peu de JavaScript (variables, fonctions, objets)
Intermediaire Comprendre les callbacks (les fonctions passées en paramètre)
| Outil | Pourquoi | Comment verifier |
|---|---|---|
| Node.js | Pour executer le code JavaScript coté serveur | node --version (doit afficher v18+) |
| Navigateur | Pour tester ton serveur | Chrome, Firefox, Edge... |
| Editeur de code | Pour ecrire le code | VS Code, Sublime Text, Notepad++... |
| Terminal | Pour lancer les commandes | Invite de commandes, PowerShell, Windows Terminal |
node --versionv20.x.x ou similaire, c'est bonOuvre un terminal et tape :
node --version
npm --version
Si tu vois des versions, c'est bon. Tu es prêt.
Debutant Commençons par le strict minimum.
Créer un serveur qui écoute sur le port 3000 et qui répond "Hello World" à toutes les requêtes. C'est le "Hello World" des serveurs web.
Crée un dossier pour ton projet, puis un fichier server.js :
// On importe le module 'http' de Node.js
// C'est un module integre, pas besoin de l'installer
const http = require('http');
// On cree un serveur
const server = http.createServer((request, response) => {
// Cette fonction est appelee a chaque requete
// On envoie le code de statut 200 (OK)
response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
// On envoie le corps de la reponse
response.end('Hello World!');
});
// Le serveur ecoute sur le port 3000
server.listen(3000, () => {
console.log('Serveur démarré sur http://localhost:3000');
});
node server.js
Tu devrais voir : Serveur démarré sur http://localhost:3000
Ouvre ton navigateur et va sur http://localhost:3000. Tu devrais voir "Hello World!".
Node.js vient avec des modules intégrés. http est l'un d'eux. Il permet de créer des serveurs HTTP et de faire des requêtes.
Cette fonction prend une fonction en paramètre. Cette fonction sera appelée à chaque fois qu'une personne se connecte au serveur.
Envoie les headers HTTP. Le premier paramètre est le code de statut (200 = OK). Le second est un objet avec les headers.
Content-Type: text/plain dit au navigateur qu'on envoie du texte simple.
charset=utf-8 dit que le texte est en UTF-8 (pour afficher les accents correctement).
Envoie le corps de la réponse et ferme la connexion. C'est la dernière chose à faire.
Le serveur commence à écouter sur le port 3000. Le callback est appelé quand le serveur est prêt.
Chaque réponse HTTP a un code de statut. Voici les plus courants :
| Code | Signification | Quand l'utiliser |
|---|---|---|
| 200 | OK | Tout s'est bien passé |
| 201 | Créé | Une ressource a été créée |
| 301 | Redirection permanente | L'URL a changé définitivement |
| 302 | Redirection temporaire | L'URL a changé temporairement |
| 400 | Mauvaise requête | Le client a envoyé n'importe quoi |
| 401 | Non autorisé | Authentification requise |
| 403 | Interdit | Tu n'as pas le droit d'accéder |
| 404 | Non trouvé | La ressource n'existe pas |
| 500 | Erreur serveur | Le serveur a planté |
Debutant Faire en sorte que le serveur réagisse différemment selon l'URL.
GET / → Page d'accueilGET /about → Page "À propos"GET /contact → Page de contactconst http = require('http');
const server = http.createServer((request, response) => {
// On recupere l'URL et la methode
const url = request.url;
const method = request.method;
// On definit le Content-Type par defaut
response.setHeader('Content-Type', 'text/html; charset=utf-8');
// Routing basique
if (url === '/' && method === 'GET') {
response.writeHead(200);
response.end(`
Page d'accueil
Bienvenue sur mon serveur !
`);
} else if (url === '/about' && method === 'GET') {
response.writeHead(200);
response.end(`
À propos
Ce serveur a été créé à la main, sans framework.
`);
} else if (url === '/contact' && method === 'GET') {
response.writeHead(200);
response.end(`
Contact
Envoyez-moi un message !
`);
} else {
// 404 - Page non trouvee
response.writeHead(404);
response.end(`
404 - Page non trouvée
L'URL ${url} n'existe pas.
Retour à l'accueil
`);
}
});
server.listen(3000, () => {
console.log('Serveur démarré sur http://localhost:3000');
});
┌─────────────────────────────────────────────────────────────┐
│ REQUETE ARRIVE │
│ (url = '/about') │
└─────────────────────────────┬───────────────────────────────┘
│
▼
┌─────────────────┐
│ url === '/' ? │──── NON ───┐
└─────────────────┘ │
│ OUI │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Page accueil │ │ url === '/about'?│
└─────────────────┘ └────────┬─────────┘
│
┌──────────────┴──────────────┐
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ Page 404 │ │ Page À propos│
└──────────┘ └──────────────┘
Pour plus de routes, on peut utiliser un objet :
const routes = {
'GET /': 'Accueil
',
'GET /about': 'À propos
',
'GET /contact': 'Contact
'
};
const server = http.createServer((req, res) => {
const key = `${req.method} ${req.url}`;
res.setHeader('Content-Type', 'text/html; charset=utf-8');
if (routes[key]) {
res.writeHead(200);
res.end(routes[key]);
} else {
res.writeHead(404);
res.end('<h1>404 - Non trouvé</h1>');
}
});
C'est plus propre pour beaucoup de routes. C'est d'ailleurs comme ça que fonctionne Express en interne.
Debutant Récupérer les paramètres dans l'URL.
C'est ce qui suit le ? dans une URL. Par exemple :
/search?q=javascript → q = "javascript"/page?page=2&limit=10 → page = "2", limit = "10"/greet?name=John&age=30 → name = "John", age = "30"const http = require('http');
const url = require('url'); // Module pour parser les URLs
const server = http.createServer((request, response) => {
// On parse l'URL
const parsedUrl = url.parse(request.url, true);
// parsedUrl.pathname = le chemin sans les query params
// parsedUrl.query = un objet avec les parametres
const pathname = parsedUrl.pathname;
const query = parsedUrl.query;
response.setHeader('Content-Type', 'text/html; charset=utf-8');
if (pathname === '/greet') {
// URL : /greet?name=John
const name = query.name || 'Visiteur';
response.writeHead(200);
response.end(`<h1>Bonjour, ${name} !</h1>`);
} else if (pathname === '/search') {
// URL : /search?q=javascript&page=1
const q = query.q || '';
const page = query.page || '1';
response.writeHead(200);
response.end(`
<h1>Recherche</h1>
<p>Terme : ${q}</p>
<p>Page : ${page}</p>
`);
} else {
response.writeHead(404);
response.end('<h1>404</h1>');
}
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
console.log('Teste : http://localhost:3000/greet?name=John');
console.log('Teste : http://localhost:3000/search?q=javascript&page=2');
});
http://localhost:3000/greet → "Bonjour, Visiteur !"http://localhost:3000/greet?name=John → "Bonjour, John !"http://localhost:3000/search?q=javascript&page=2 → Terme: javascript, Page: 2Le module url de Node.js permet de parser une URL en ses composants :
const url = require('url');
const myUrl = 'http://example.com/search?q=javascript&page=2#section';
const parsed = url.parse(myUrl, true);
console.log(parsed.pathname); // '/search'
console.log(parsed.query); // { q: 'javascript', page: '2' }
console.log(parsed.hash); // '#section'
console.log(parsed.host); // 'example.com'
console.log(parsed.protocol); // 'http:'
Note : Le deuxième paramètre true indique de parser les query params en objet. Sinon, c'est une string.
Intermediaire Récupérer les données envoyées dans le corps d'une requête POST.
Contrairement aux query params qui sont dans l'URL, les données POST sont dans le body de la requête. Et ce body, il n'est pas disponible directement.
Il faut le lire en morceaux (chunks), l'assembler, puis le parser.
const http = require('http');
const url = require('url');
// Fonction pour parser le body JSON
function parseJsonBody(request) {
return new Promise((resolve, reject) => {
let body = '';
// Les donnees arrivent par morceaux (chunks)
request.on('data', (chunk) => {
body += chunk.toString();
});
// Quand toutes les donnees sont arrivees
request.on('end', () => {
try {
if (body) {
resolve(JSON.parse(body));
} else {
resolve({});
}
} catch (error) {
reject(error);
}
});
request.on('error', reject);
});
}
const server = http.createServer(async (request, response) => {
const parsedUrl = url.parse(request.url, true);
const pathname = parsedUrl.pathname;
const method = request.method;
response.setHeader('Content-Type', 'application/json; charset=utf-8');
// Route GET - Afficher un formulaire
if (pathname === '/' && method === 'GET') {
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
response.end(`
<h1>Envoyer des donnees</h1>
<form action="/submit" method="POST">
<p>
<label>Nom : <input type="text" name="name" required></label>
</p>
<p>
<label>Email : <input type="email" name="email" required></label>
</p>
<button type="submit">Envoyer</button>
</form>
<hr>
<h2>Ou envoyer du JSON</h2>
<pre>curl -X POST http://localhost:3000/json \\
-H "Content-Type: application/json" \\
-d '{"name":"John","email":"john@example.com"}'</pre>
`);
return;
}
// Route POST - JSON
if (pathname === '/json' && method === 'POST') {
try {
const data = await parseJsonBody(request);
response.writeHead(200);
response.end(JSON.stringify({
success: true,
message: 'Données reçues',
data: data
}));
} catch (error) {
response.writeHead(400);
response.end(JSON.stringify({
success: false,
error: 'JSON invalide'
}));
}
return;
}
// Route POST - Formulaire (urlencoded)
if (pathname === '/submit' && method === 'POST') {
let body = '';
request.on('data', (chunk) => {
body += chunk.toString();
});
request.on('end', () => {
// Parser les donnees urlencoded
// Format: name=John&email=john@example.com
const params = new URLSearchParams(body);
const data = Object.fromEntries(params);
response.writeHead(200);
response.end(JSON.stringify({
success: true,
message: 'Formulaire reçu',
data: data
}));
});
return;
}
// 404
response.writeHead(404);
response.end(JSON.stringify({ error: 'Non trouvé' }));
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});
# Envoyer du JSON
curl -X POST http://localhost:3000/json -H "Content-Type: application/json" -d "{\"name\":\"John\",\"email\":\"john@example.com\"}"
application/x-www-form-urlencoded par défaut. C'est pour ça qu'on utilise URLSearchParams pour les parser.
Le body d'une requête HTTP peut être très gros (upload de fichier, gros JSON...). Si Node.js le chargeait tout en mémoire d'un coup, le serveur pourrait planter.
À la place, Node.js envoie les données par petits morceaux (chunks) via des événements :
data : un morceau est arrivéend : tous les morceaux sont arrivéserror : une erreur est survenueC'est la même logique que les streams dans Node.js.
Debutant Servir des fichiers HTML, CSS, JS, images...
Jusqu'ici, on a envoyé du HTML directement dans le code. Mais si tu veux servir un vrai fichier HTML, une image, ou du CSS, il faut lire le fichier depuis le disque et l'envoyer au client.
mon-serveur/
├── server.js
└── public/
├── index.html
├── style.css
└── image.jpg
const http = require('http');
const fs = require('fs');
const path = require('path');
// Dossier des fichiers statiques
const PUBLIC_DIR = path.join(__dirname, 'public');
// Map des types MIME
const MIME_TYPES = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.txt': 'text/plain',
'.pdf': 'application/pdf'
};
const server = http.createServer((request, response) => {
// Si l'URL est '/', on sert index.html
let filePath = request.url === '/'
? path.join(PUBLIC_DIR, 'index.html')
: path.join(PUBLIC_DIR, request.url);
// Securite : empecher de sortir du dossier public
if (!filePath.startsWith(PUBLIC_DIR)) {
response.writeHead(403);
response.end('Interdit');
return;
}
// Determiner le type MIME
const ext = path.extname(filePath);
const contentType = MIME_TYPES[ext] || 'application/octet-stream';
// Lire le fichier
fs.readFile(filePath, (err, data) => {
if (err) {
if (err.code === 'ENOENT') {
// Fichier non trouve
response.writeHead(404);
response.end('Fichier non trouvé');
} else {
// Autre erreur
response.writeHead(500);
response.end('Erreur serveur');
}
return;
}
// Envoyer le fichier
response.writeHead(200, { 'Content-Type': contentType });
response.end(data);
});
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
console.log('Fichiers servis depuis le dossier public/');
});
Dans le dossier public/, crée index.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mon serveur</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Bienvenue !</h1>
<p>Ce fichier est servi par ton serveur Node.js.</p>
<img src="image.jpg" alt="Une image">
</body>
</html>
Et public/style.css :
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
}
h1 {
color: #333;
}
img {
max-width: 100%;
border-radius: 8px;
}
public/. Sans ça, un utilisateur pourrait demander ../../../etc/passwd et lire des fichiers sensibles.
Intermediaire Remplacer des variables dans du HTML.
Un template, c'est un fichier HTML avec des "trous" à combler. Par exemple :
<h1>Bonjour, {{name}} !</h1>
Quand on remplace {{name}} par John, on obtient :
<h1>Bonjour, John !</h1>
const http = require('http');
const fs = require('fs');
const path = require('path');
const TEMPLATES_DIR = path.join(__dirname, 'templates');
// Moteur de template minimaliste
function renderTemplate(templateName, data) {
return new Promise((resolve, reject) => {
const filePath = path.join(TEMPLATES_DIR, templateName);
fs.readFile(filePath, 'utf-8', (err, template) => {
if (err) {
reject(err);
return;
}
// Remplacer {{variable}} par sa valeur
let html = template;
for (const [key, value] of Object.entries(data)) {
const regex = new RegExp(`{{${key}}}`, 'g');
html = html.replace(regex, value);
}
resolve(html);
});
});
}
const server = http.createServer(async (request, response) => {
const url = request.url;
response.setHeader('Content-Type', 'text/html; charset=utf-8');
try {
if (url === '/' || url === '/home') {
const html = await renderTemplate('home.html', {
title: 'Page d\'accueil',
message: 'Bienvenue sur mon site !',
year: new Date().getFullYear()
});
response.writeHead(200);
response.end(html);
} else if (url === '/about') {
const html = await renderTemplate('about.html', {
title: 'À propos',
author: 'Moi',
year: new Date().getFullYear()
});
response.writeHead(200);
response.end(html);
} else {
const html = await renderTemplate('404.html', {
title: 'Page non trouvée'
});
response.writeHead(404);
response.end(html);
}
} catch (error) {
console.error(error);
response.writeHead(500);
response.end('<h1>Erreur serveur</h1>');
}
});
server.listen(3000, () => {
console.log('Serveur sur http://localhost:3000');
});
Crée templates/home.html :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>{{title}}</title>
</head>
<body>
<h1>{{message}}</h1>
<p>Année : {{year}}</p>
<nav>
<a href="/">Accueil</a> |
<a href="/about">À propos</a>
</nav>
</body>
</html>
Intermediaire Le concept fondamental utilisé par Express.
Un middleware, c'est une fonction qui s'exécute entre la requête et la réponse. Il peut :
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Requête │───▶│Middleware│───▶│Middleware│───▶│Middleware│───▶│ Route │
│ │ │ 1 │ │ 2 │ │ 3 │ │ finale │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ Réponse │
└──────────┘
const http = require('http');
// Notre "app" - un mini-framework maison
const app = {
middlewares: [],
// Ajouter un middleware
use(middleware) {
this.middlewares.push(middleware);
},
// Executer les middlewares
handle(request, response) {
let index = 0;
const next = () => {
if (index < this.middlewares.length) {
const middleware = this.middlewares[index];
index++;
middleware(request, response, next);
}
};
next();
}
};
// --- MIDDLEWARES ---
// 1. Logger - affiche chaque requete
app.use((req, res, next) => {
const time = new Date().toISOString();
console.log(`[${time}] ${req.method} ${req.url}`);
next();
});
// 2. Parser JSON body
app.use((req, res, next) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
req.body = body ? JSON.parse(body) : {};
next();
} catch (e) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'JSON invalide' }));
}
});
});
// 3. Ajouter des headers CORS
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
next();
});
// 4. Route finale
app.use((req, res) => {
res.setHeader('Content-Type', 'application/json');
if (req.url === '/' && req.method === 'GET') {
res.writeHead(200);
res.end(JSON.stringify({ message: 'Bienvenue' }));
} else if (req.url === '/api/data' && req.method === 'POST') {
res.writeHead(200);
res.end(JSON.stringify({ success: true, received: req.body }));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Non trouvé' }));
}
});
// --- SERVEUR ---
const server = http.createServer((req, res) => {
app.handle(req, res);
});
server.listen(3000, () => {
console.log('Serveur avec middlewares sur http://localhost:3000');
console.log('Teste : curl http://localhost:3000/');
console.log('Teste : curl -X POST http://localhost:3000/api/data -H "Content-Type: application/json" -d \'{"test":"ok"}\'');
});
app.use() ajoute un middleware à la pile, et next() passe au suivant.
Intermediaire Créer une API REST avec Create, Read, Update, Delete.
Créer une API pour gérer une liste de tâches (todos) :
GET /api/todos → Liste toutes les tâchesGET /api/todos/:id → Une tâche spécifiquePOST /api/todos → Créer une tâchePUT /api/todos/:id → Modifier une tâcheDELETE /api/todos/:id → Supprimer une tâcheconst http = require('http');
const url = require('url');
// Base de donnees en memoire (tres simple, pas persistante)
let todos = [
{ id: 1, title: 'Apprendre Node.js', done: false },
{ id: 2, title: 'Créer un serveur web', done: true },
{ id: 3, title: 'Comprendre HTTP', done: false }
];
let nextId = 4;
// Helper pour parser le body
function parseBody(req) {
return new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch {
resolve({});
}
});
});
}
// Helper pour envoyer du JSON
function sendJson(res, status, data) {
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify(data));
}
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
const pathname = parsedUrl.pathname;
const method = req.method;
// CORS
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
// Preflight pour CORS
if (method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Router
// GET /api/todos - Liste toutes les todos
if (pathname === '/api/todos' && method === 'GET') {
sendJson(res, 200, { success: true, data: todos });
return;
}
// GET /api/todos/:id - Une todo spécifique
const matchGet = pathname.match(/^\/api\/todos\/(\d+)$/);
if (matchGet && method === 'GET') {
const id = parseInt(matchGet[1]);
const todo = todos.find(t => t.id === id);
if (todo) {
sendJson(res, 200, { success: true, data: todo });
} else {
sendJson(res, 404, { success: false, error: 'Tâche non trouvée' });
}
return;
}
// POST /api/todos - Créer une todo
if (pathname === '/api/todos' && method === 'POST') {
const body = await parseBody(req);
if (!body.title) {
sendJson(res, 400, { success: false, error: 'Le titre est requis' });
return;
}
const newTodo = {
id: nextId++,
title: body.title,
done: body.done || false
};
todos.push(newTodo);
sendJson(res, 201, { success: true, data: newTodo });
return;
}
// PUT /api/todos/:id - Modifier une todo
const matchPut = pathname.match(/^\/api\/todos\/(\d+)$/);
if (matchPut && method === 'PUT') {
const id = parseInt(matchPut[1]);
const body = await parseBody(req);
const todo = todos.find(t => t.id === id);
if (!todo) {
sendJson(res, 404, { success: false, error: 'Tâche non trouvée' });
return;
}
if (body.title !== undefined) todo.title = body.title;
if (body.done !== undefined) todo.done = body.done;
sendJson(res, 200, { success: true, data: todo });
return;
}
// DELETE /api/todos/:id - Supprimer une todo
const matchDelete = pathname.match(/^\/api\/todos\/(\d+)$/);
if (matchDelete && method === 'DELETE') {
const id = parseInt(matchDelete[1]);
const index = todos.findIndex(t => t.id === id);
if (index === -1) {
sendJson(res, 404, { success: false, error: 'Tâche non trouvée' });
return;
}
todos.splice(index, 1);
sendJson(res, 200, { success: true, message: 'Tâche supprimée' });
return;
}
// Page d'accueil
if (pathname === '/' && method === 'GET') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<h1>API Todos</h1>
<h2>Endpoints</h2>
<ul>
<li>GET /api/todos</li>
<li>GET /api/todos/:id</li>
<li>POST /api/todos { "title": "..." }</li>
<li>PUT /api/todos/:id { "title": "...", "done": true }</li>
<li>DELETE /api/todos/:id</li>
</ul>
`);
return;
}
// 404
sendJson(res, 404, { success: false, error: 'Non trouvé' });
});
server.listen(3000, () => {
console.log('API Todos sur http://localhost:3000');
console.log('');
console.log('Endpoints:');
console.log(' GET /api/todos - Liste');
console.log(' GET /api/todos/1 - Détail');
console.log(' POST /api/todos - Créer');
console.log(' PUT /api/todos/1 - Modifier');
console.log(' DELETE /api/todos/1 - Supprimer');
});
# Lister les todos
curl http://localhost:3000/api/todos
# Créer une todo
curl -X POST http://localhost:3000/api/todos -H "Content-Type: application/json" -d "{\"title\":\"Nouvelle tâche\"}"
# Modifier une todo
curl -X PUT http://localhost:3000/api/todos/1 -H "Content-Type: application/json" -d "{\"done\":true}"
# Supprimer une todo
curl -X DELETE http://localhost:3000/api/todos/1
Intermediaire Comment savoir qui est qui entre deux requêtes ?
HTTP est stateless : chaque requête est indépendante. Le serveur ne se souvient pas de la requête précédente.
Pour garder un état (utilisateur connecté, panier d'achat...), on utilise des sessions.
const http = require('http');
const crypto = require('crypto');
// Stockage des sessions en memoire
// En production, utiliser Redis ou une base de donnees
const sessions = new Map();
// Generer un ID de session unique
function generateSessionId() {
return crypto.randomBytes(16).toString('hex');
}
// Parser les cookies
function parseCookies(cookieHeader) {
const cookies = {};
if (!cookieHeader) return cookies;
cookieHeader.split(';').forEach(cookie => {
const [name, value] = cookie.trim().split('=');
cookies[name] = value;
});
return cookies;
}
// Helper pour parser le body
function parseBody(req) {
return new Promise((resolve) => {
let body = '';
req.on('data', (chunk) => { body += chunk; });
req.on('end', () => {
try {
resolve(body ? JSON.parse(body) : {});
} catch {
resolve({});
}
});
});
}
const server = http.createServer(async (req, res) => {
const cookies = parseCookies(req.headers.cookie);
// Recuperer ou creer une session
let sessionId = cookies.sessionId;
let session = sessions.get(sessionId);
if (!session) {
sessionId = generateSessionId();
session = { views: 0, user: null };
sessions.set(sessionId, session);
}
res.setHeader('Content-Type', 'text/html; charset=utf-8');
// Page d'accueil
if (req.url === '/' && req.method === 'GET') {
session.views++;
// Definir le cookie de session
res.setHeader('Set-Cookie', `sessionId=${sessionId}; HttpOnly; Path=/`);
res.writeHead(200);
res.end(`
<h1>Sessions Demo</h1>
<p>Session ID: ${sessionId}</p>
<p>Pages vues: ${session.views}</p>
<p>Utilisateur: ${session.user || 'Non connecté'}</p>
<hr>
<form action="/login" method="POST">
<input type="text" name="username" placeholder="Username">
<button type="submit">Login</button>
</form>
<br>
<a href="/logout">Logout</a>
`);
return;
}
// Login
if (req.url === '/login' && req.method === 'POST') {
const body = await parseBody(req);
// En réalité, il faudrait verifier le mot de passe
session.user = body.username || 'Anonyme';
res.setHeader('Set-Cookie', `sessionId=${sessionId}; HttpOnly; Path=/`);
res.writeHead(302, { 'Location': '/' });
res.end();
return;
}
// Logout
if (req.url === '/logout' && req.method === 'GET') {
sessions.delete(sessionId);
res.writeHead(302, { 'Location': '/' });
res.end();
return;
}
// 404
res.writeHead(404);
res.end('<h1>404</h1>');
});
server.listen(3000, () => {
console.log('Serveur avec sessions sur http://localhost:3000');
console.log('Ouvre le navigateur et rafraichis plusieurs fois.');
console.log('Tu verras le compteur de pages vues augmenter.');
});
Avance Sécuriser les communications avec SSL/TLS.
HTTP envoie les données en clair. N'importe qui sur le réseau peut les intercepter. HTTPS chiffre les communications.
Pour le développement, on peut créer un certificat auto-signé :
# Generer une cle privee
openssl genrsa -out key.pem 2048
# Generer un certificat auto-signé
openssl req -new -x509 -key key.pem -out cert.pem -days 365
# Repondre aux questions (ou appuyer sur Entree pour laisser vide)
const https = require('https');
const fs = require('fs');
const path = require('path');
// Charger le certificat et la cle
const options = {
key: fs.readFileSync(path.join(__dirname, 'key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'cert.pem'))
};
const server = https.createServer(options, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(`
<h1>HTTPS fonctionne !</h1>
<p>Tu accèdes à cette page en HTTPS.</p>
<p>Le cadenas à côté de l'URL indique que la connexion est sécurisée.</p>
`);
});
server.listen(443, () => {
console.log('Serveur HTTPS sur https://localhost');
console.log('Note: Sur Windows, il faut peut-être lancer en administrateur pour le port 443');
});
Si le port 443 est déjà utilisé ou nécessite des privilèges :
server.listen(8443, () => {
console.log('Serveur HTTPS sur https://localhost:8443');
});
Pour forcer HTTPS, on peut rediriger toutes les requêtes HTTP vers HTTPS :
const http = require('http');
const https = require('https');
const fs = require('fs');
// Serveur HTTP qui redirige vers HTTPS
const httpServer = http.createServer((req, res) => {
res.writeHead(301, { 'Location': `https://localhost${req.url}` });
res.end();
});
// Serveur HTTPS
const httpsOptions = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
const httpsServer = https.createServer(httpsOptions, (req, res) => {
res.writeHead(200);
res.end('HTTPS OK');
});
httpServer.listen(80, () => console.log('HTTP sur port 80 (redirect)'));
httpsServer.listen(443, () => console.log('HTTPS sur port 443'));
Debutant Containeriser ton serveur pour le déployer facilement.
Crée un fichier Dockerfile :
# Image de base avec Node.js
FROM node:20-alpine
# Dossier de travail dans le conteneur
WORKDIR /app
# Copier les fichiers de dépendances (si package.json existe)
COPY package*.json ./
# Installer les dépendances (si nécessaire)
RUN npm ci --only=production || true
# Copier le code
COPY . .
# Exposer le port
EXPOSE 3000
# Commande de démarrage
CMD ["node", "server.js"]
services:
webserver:
build: .
container_name: mon-serveur-web
restart: unless-stopped
ports:
- "3000:3000"
environment:
- NODE_ENV=production
volumes:
- ./public:/app/public
- ./templates:/app/templates
# Construire l'image
docker build -t mon-serveur-web .
# Lancer le conteneur
docker run -d -p 3000:3000 --name webserver mon-serveur-web
# Ou avec docker-compose
docker-compose up -d
# Voir les logs
docker logs webserver
# Arrêter le conteneur
docker stop webserver
Si le port est pris par un autre programme :
# Changer le port dans le code
server.listen(3001, () => {
console.log('Serveur sur http://localhost:3001');
});
Ou tuer le processus qui utilise le port :
# Windows
netstat -ano | findstr :3000
taskkill /PID <pid> /F
# Linux/Mac
lsof -i :3000
kill -9 <pid>
Cette erreur signifie que le port est déjà utilisé. Voir la solution ci-dessus.
Le module http est intégré à Node.js. Si tu as cette erreur, tu es probablement dans un navigateur, pas dans Node.js.
Le code de ce tutoriel s'exécute coté serveur, pas dans un navigateur.
Ajoute charset=utf-8 dans le Content-Type :
response.setHeader('Content-Type', 'text/html; charset=utf-8');
Vérifie le chemin des fichiers. Utilise __dirname pour les chemins absolus :
const path = require('path');
const filePath = path.join(__dirname, 'public', 'index.html');
Les certificats auto-signés ne sont pas reconnus par les navigateurs. Tu verras un avertissement "Connexion non sécurisée".
Pour le développement, tu peux cliquer sur "Avancé" puis "Continuer quand même".
Pour la production, utilise Let's Encrypt (gratuit) ou un certificat payant.
Tu as maintenant les bases pour comprendre les frameworks comme Express. Voici ce que tu peux faire :
Voici ce que fait Express automatiquement que tu as fait à la main :
| Fonctionnalité | Node.js natif | Express |
|---|---|---|
| Routing | if/else manuel |
app.get('/path', ...) |
| Body parsing | req.on('data') manuel |
express.json() middleware |
| Fichiers statiques | fs.readFile() manuel |
express.static('public') |
| Middlewares | À implémenter | Système intégré |
| Gestion d'erreurs | Manuel | Error handler intégré |
Maintenant que tu sais comment ça marche "en dessous", tu peux utiliser Express en connaissance de cause.
app.get('/', ...) dans Express. C'est pas de la magie, c'est juste du code qui gère les routes, parse le body, et appelle les middlewares les uns après les autres.