1. Qu'est-ce qu'un serveur web ?

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 :

  1. Tu ouvres ton navigateur et tapes une URL (par exemple https://google.com)
  2. Ton navigateur envoie une demande (appelée "requete HTTP") au serveur de Google
  3. Le serveur recoit la demande et comprend que tu veux la page d'accueil
  4. Il prepare la reponse : il va chercher les fichiers HTML, CSS, JavaScript
  5. Il te renvoie tout ça (appelé "reponse HTTP")
  6. Ton navigateur affiche la page que tu vois

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.

A quoi ça sert concretement ?

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
En resume : Un serveur web, c'est un programme qui ecoute les demandes qui arrivent sur un port (comme le 80 ou le 443), et qui repond avec des fichiers ou des donnees. C'est aussi simple que ça.

2. Pourquoi construire le sien ?

Tu vas me dire : "Mais pourquoi faire ça à la main ? Express existe déjà !"

Bonne question. Voici pourquoi :

1. Comprendre ce qui se passe sous le capot

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.

2. Debug plus facilement

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."

3. Avoir un avis sur les frameworks

Tu pourras comprendre les différences entre Express, Fastify, Koa, Hono... Et choisir le bon outil pour le bon projet.

4. C'est formateur

C'est comme apprendre à conduire. Tu peux prendre des cours auto-école (frameworks), mais comprendre comment fonctionne un moteur ça aide à mieux conduire.

Est-ce que je dois faire ça en production ?

Non. Ce tutoriel est pédagogique. Pour un vrai projet, utilise Express, Fastify, ou un autre framework. Ils gèrent les cas limites, la sécurité, les performances que nous n'aborderons pas ici.

Ce serveur "from scratch" est pour apprendre. Pas pour mettre en production sur un site à 1 million d'utilisateurs.

3. Comparaison : Node.js natif vs Express vs Fastify

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
En gros : Node.js natif = comprendre. Express/Fastify = être productif. Après ce tuto, tu sauras pourquoi Express existe et ce qu'il fait pour toi.

4. Prerequis

Ce qu'il faut savoir avant

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)

Ce que tu n'as PAS besoin de savoir

  • Comment marche HTTP en détail (on va apprendre ensemble)
  • Express, Fastify, ou tout framework
  • Docker (on verra dans une section à part)
  • Les certificats SSL (on verra dans la section HTTPS)

Outils necessaires

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
Installer Node.js (si pas encore fait)
  1. Va sur nodejs.org
  2. Telecharge la version LTS (Long Term Support)
  3. Installe-le en cliquant sur "Next" partout
  4. Ouvre un terminal et tape node --version
  5. Si tu vois v20.x.x ou similaire, c'est bon

Verifier que tout fonctionne

Ouvre un terminal et tape :

node --version
npm --version

Si tu vois des versions, c'est bon. Tu es prêt.

5. Hello World - Premier serveur

Debutant Commençons par le strict minimum.

Ce qu'on va faire

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.

Le code

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');
});

Lancer le serveur

node server.js

Tu devrais voir : Serveur démarré sur http://localhost:3000

Tester

Ouvre ton navigateur et va sur http://localhost:3000. Tu devrais voir "Hello World!".

Bravo ! Tu viens de créer ton premier serveur web. En 10 lignes de code. C'est ça la base.
Explications detaillees

Le module 'http'

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.

http.createServer()

Cette fonction prend une fonction en paramètre. Cette fonction sera appelée à chaque fois qu'une personne se connecte au serveur.

request et response

  • request : C'est ce que le navigateur envoie. Contient l'URL, la méthode (GET, POST...), les headers, le body...
  • response : C'est ce que tu envoies au navigateur. Contient le statut, les headers, le corps de la réponse...

response.writeHead()

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).

response.end()

Envoie le corps de la réponse et ferme la connexion. C'est la dernière chose à faire.

server.listen()

Le serveur commence à écouter sur le port 3000. Le callback est appelé quand le serveur est prêt.

Comprendre les codes de statut HTTP

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é

6. Routing - Gerer les routes

Debutant Faire en sorte que le serveur réagisse différemment selon l'URL.

Ce qu'on veut

  • GET / → Page d'accueil
  • GET /about → Page "À propos"
  • GET /contact → Page de contact
  • Autres URLs → Erreur 404

Le code

const 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'); });

Ce qui se passe

┌─────────────────────────────────────────────────────────────┐
│                    REQUETE ARRIVE                            │
│                  (url = '/about')                            │
└─────────────────────────────┬───────────────────────────────┘
                              │
                              ▼
                    ┌─────────────────┐
                    │   url === '/' ? │──── NON ───┐
                    └─────────────────┘            │
                              │ OUI                 │
                              ▼                     ▼
                    ┌─────────────────┐    ┌──────────────────┐
                    │  Page accueil   │    │ url === '/about'?│
                    └─────────────────┘    └────────┬─────────┘
                                                   │
                                    ┌──────────────┴──────────────┐
                                    │                              │
                                    ▼                              ▼
                              ┌──────────┐                ┌──────────────┐
                              │ Page 404 │                │ Page À propos│
                              └──────────┘                └──────────────┘
          
Le routing avec if/else C'est basique, mais ça fonctionne. Chaque framework fait essentiellement la même chose, mais avec une syntaxe plus élégante.
Optimisation : utiliser un objet de routes

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.

7. Query params - Parametres d'URL

Debutant Récupérer les paramètres dans l'URL.

Qu'est-ce qu'un query param ?

C'est ce qui suit le ? dans une URL. Par exemple :

  • /search?q=javascriptq = "javascript"
  • /page?page=2&limit=10page = "2", limit = "10"
  • /greet?name=John&age=30name = "John", age = "30"

Le code

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');
});

Tester

  • 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: 2
Comment fonctionne url.parse() ?

Le 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.

8. Body parsing - Recevoir des donnees

Intermediaire Récupérer les données envoyées dans le corps d'une requête POST.

Le problème

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.

Le code

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');
});

Comment tester avec curl

# Envoyer du JSON
curl -X POST http://localhost:3000/json -H "Content-Type: application/json" -d "{\"name\":\"John\",\"email\":\"john@example.com\"}"
Note : Les données du formulaire HTML sont envoyées au format application/x-www-form-urlencoded par défaut. C'est pour ça qu'on utilise URLSearchParams pour les parser.
Pourquoi les chunks ?

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és
  • error : une erreur est survenue

C'est la même logique que les streams dans Node.js.

9. Fichiers statiques

Debutant Servir des fichiers HTML, CSS, JS, images...

Le problème

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.

Structure des dossiers

mon-serveur/
├── server.js
└── public/
    ├── index.html
    ├── style.css
    └── image.jpg

Le code

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/');
});

Créer les fichiers de test

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;
}
Attention à la sécurité : Ce code vérifie que le fichier demandé est bien dans le dossier public/. Sans ça, un utilisateur pourrait demander ../../../etc/passwd et lire des fichiers sensibles.

10. Templates - Pages dynamiques

Intermediaire Remplacer des variables dans du HTML.

Le concept

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>

Le code

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');
});

Template d'exemple

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>
C'est comme ça que fonctionne EJS, Handlebars, Pug... Sauf qu'ils ont plus de fonctionnalités (boucles, conditions, partials, etc.). Mais le principe de base est le même : remplacer des placeholders par des valeurs.

11. Middlewares - Le pattern

Intermediaire Le concept fondamental utilisé par Express.

Qu'est-ce qu'un middleware ?

Un middleware, c'est une fonction qui s'exécute entre la requête et la réponse. Il peut :

  • Modifier la requête (ajouter des infos)
  • Modifier la réponse
  • Faire des logs
  • Vérifier l'authentification
  • Passer au middleware suivant
┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ Requête  │───▶│Middleware│───▶│Middleware│───▶│Middleware│───▶│ Route    │
│          │    │   1      │    │   2      │    │   3      │    │ finale   │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘
                                                     │
                                                     ▼
                                               ┌──────────┐
                                               │ Réponse  │
                                               └──────────┘
          

Le code

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"}\'');
});
Ça te dit quelque chose ? C'est exactement comme ça que fonctionne Express ! app.use() ajoute un middleware à la pile, et next() passe au suivant.

12. API JSON - CRUD simple

Intermediaire Créer une API REST avec Create, Read, Update, Delete.

Objectif

Créer une API pour gérer une liste de tâches (todos) :

  • GET /api/todos → Liste toutes les tâches
  • GET /api/todos/:id → Une tâche spécifique
  • POST /api/todos → Créer une tâche
  • PUT /api/todos/:id → Modifier une tâche
  • DELETE /api/todos/:id → Supprimer une tâche

Le code

const 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');
});

Tester l'API

# 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

13. Sessions - Garder un etat

Intermediaire Comment savoir qui est qui entre deux requêtes ?

Le problème

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.

Comment ça marche ?

  1. L'utilisateur se connecte
  2. Le serveur crée une session avec un ID unique
  3. Le serveur envoie cet ID au client dans un cookie
  4. À chaque requête suivante, le client renvoie le cookie
  5. Le serveur retrouve la session grâce à l'ID

Le code

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.');
});
Stockage en mémoire : Ce code stocke les sessions en mémoire. Si le serveur redémarre, toutes les sessions sont perdues. En production, utilise Redis, une base de données, ou un système de sessions persistant.

14. HTTPS - Securite

Avance Sécuriser les communications avec SSL/TLS.

Pourquoi HTTPS ?

HTTP envoie les données en clair. N'importe qui sur le réseau peut les intercepter. HTTPS chiffre les communications.

Créer un certificat auto-signé

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)
Note : Pour un certificat valide en production, utilise Let's Encrypt (gratuit) ou achète un certificat.

Le code HTTPS

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');
});
Port alternatif si le 443 est réservé

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');
});
HTTP vers HTTPS : rediriger automatiquement

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'));

15. Docker - Deploiement

Debutant Containeriser ton serveur pour le déployer facilement.

Dockerfile

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"]

docker-compose.yml

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

Lancer avec Docker

# 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
Pour ton setup : Tu peux ensuite configurer Nginx Proxy Manager pour rediriger un domaine vers ce conteneur, comme tu le fais pour Emby ou n8n.

16. Depannage

Le port 3000 est déjà utilisé

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>
Erreur "EADDRINUSE"

Cette erreur signifie que le port est déjà utilisé. Voir la solution ci-dessus.

"Cannot find module 'http'"

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.

Les accents ne s'affichent pas

Ajoute charset=utf-8 dans le Content-Type :

response.setHeader('Content-Type', 'text/html; charset=utf-8');
Les fichiers ne sont pas trouvés (404)

Vérifie le chemin des fichiers. Utilise __dirname pour les chemins absolus :

const path = require('path');
const filePath = path.join(__dirname, 'public', 'index.html');
Le certificat HTTPS n'est pas reconnu par le navigateur

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.

17. Pour aller plus loin

Tu as maintenant les bases pour comprendre les frameworks comme Express. Voici ce que tu peux faire :

Prochaines étapes

  • Apprendre Express.js — Le framework le plus populaire pour Node.js
  • Apprendre Fastify — Plus rapide et moderne
  • Connecter une base de données — PostgreSQL, MongoDB, SQLite...
  • Ajouter l'authentification — JWT, OAuth, sessions...
  • Déployer en production — PM2, Nginx, HTTPS...

Ressources

Comparer avec Express

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.

Et voilà ! Tu as construit ton propre serveur web de zéro. Tu comprends maintenant ce qui se passe quand tu fais 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.
Retour aux tutoriels