TW3 — Technologies du Web 3

Atelier — Construire un serveur HTTP pas à pas

Partir d'un serveur minimal et l'améliorer progressivement : routes multiples, parsing de corps, fichiers statiques, gestion d'erreurs.

Cet atelier met en pratique les acquis des 4 pages Apport. Vous partez d'un serveur HTTP minimal et l'améliorez pas à pas : routes multiples, parsing de corps JSON, serveur de fichiers statiques, gestion d'erreurs propre.

Point de départ — un serveur minimal

Voici le serveur vu dans la page Runtime et modules :

import http from "node:http"
import type { IncomingMessage, ServerResponse } from "node:http"

const PORT = 3000

const server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
  res.writeHead(200, { "Content-Type": "text/plain" })
  res.end(`Bonjour, méthode ${req.method}, chemin ${req.url}`)
})

server.listen(PORT, () => {
  console.log(`Serveur écoute sur http://localhost:${PORT}`)
})

Lancez-le avec npx tsx src/index.ts et testez avec curl :

curl -i http://localhost:3000/api/ping
# → 200 Bonjour, méthode GET, chemin /api/ping

Étape 1 — Routes multiples

Remplacez la réponse générique par un vrai routage. Répondez différemment selon l'URL :

import http from "node:http"
import type { IncomingMessage, ServerResponse } from "node:http"

const PORT = 3000

const server = http.createServer((req: IncomingMessage, res: ServerResponse) => {
  const url = req.url.split("?")[0]
  const method = req.method

  if (url === "/api/ping" && method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ status: "ok", timestamp: Date.now() }))
    return
  }

  if (url === "/api/health" && method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ status: "healthy" }))
    return
  }

  // Route inconnue
  res.writeHead(404, { "Content-Type": "application/json" })
  res.end(JSON.stringify({ error: "Not found" }))
})

server.listen(PORT, () => {
  console.log(`Serveur écoute sur http://localhost:${PORT}`)
})

Testez :

curl -s http://localhost:3000/api/ping     # → {"status":"ok","timestamp":...}
curl -s http://localhost:3000/api/health   # → {"status":"healthy"}
curl -s http://localhost:3000/unknown      # → {"error":"Not found"}
curl -s -X POST http://localhost:3000/api/ping  # → {"error":"Not found"} ( méthod e non gérée)

Pour l'instant, les méthodes non gérées sur une route connue renvoient aussi 404. L'étape 3 améliorera cela avec un 405.


Étape 2 — Parser le corps d'une requête POST

Pour lire un corps JSON envoyé par le client, il faut accumuler les chunks dès la page Serveur HTTP natif :

import http from "node:http"
import type { IncomingMessage, ServerResponse } from "node:http"

function lireCorps(req: IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    let corps = ""
    req.on("data", (chunk: string) => { corps += chunk })
    req.on("end", () => resolve(corps))
    req.on("error", (err) => reject(err))
  })
}

const PORT = 3000

const server = http.createServer(async (req: IncomingMessage, res: ServerResponse) => {
  const url = req.url.split("?")[0]
  const method = req.method

  if (url === "/api/items" && method === "POST") {
    try {
      const corps = await lireCorps(req)
      const data = JSON.parse(corps) as { name: string; qty: number }
      res.writeHead(201, { "Content-Type": "application/json" })
      res.end(JSON.stringify({ id: Date.now(), ...data }))
    } catch {
      res.writeHead(400, { "Content-Type": "application/json" })
      res.end(JSON.stringify({ error: "Corps JSON invalide" }))
    }
    return
  }

  if (url === "/api/items" && method === "GET") {
    res.writeHead(200, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ items: [] }))
    return
  }

  res.writeHead(404, { "Content-Type": "application/json" })
  res.end(JSON.stringify({ error: "Not found" }))
})

server.listen(PORT, () => {
  console.log(`Serveur écoute sur http://localhost:${PORT}`)
})

Testez avec curl :

curl -s -X POST http://localhost:3000/api/items \
  -H "Content-Type: application/json" \
  -d '{"name":"Visa", "qty": 5}'
# → {"id":1735648291234,"name":"Visa","qty":5}

curl -s -X POST http://localhost:3000/api/items \
  -H "Content-Type: application/json" \
  -d 'pas du json'
# → {"error":"Corps JSON invalide"}

Étape 3 — Codes HTTP précis (404 vs 405)

Séparez les routes inconnues (404) des méthodes non gérées sur une route connue (405) :

import http from "node:http"
import type { IncomingMessage, ServerResponse } from "node:http"

function lireCorps(req: IncomingMessage): Promise<string> {
  return new Promise((resolve, reject) => {
    let corps = ""
    req.on("data", (chunk: string) => { corps += chunk })
    req.on("end", () => resolve(corps))
    req.on("error", (err) => reject(err))
  })
}

const server = http.createServer(async (req: IncomingMessage, res: ServerResponse) => {
  const url = req.url.split("?")[0]
  const method = req.method
  const routesConnues = ["/api/ping", "/api/health", "/api/items"]

  if (!routesConnues.includes(url)) {
    // Route totalement inconnue → 404
    res.writeHead(404, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ error: "Not found" }))
    return
  }

  // Route connue mais méthode non gérée → 405
  if (url === "/api/items" && method !== "GET" && method !== "POST") {
    res.writeHead(405, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ error: `Method ${method} not allowed` }))
    return
  }

  if (url === "/api/ping" && method !== "GET") {
    res.writeHead(405, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ error: `Method ${method} not allowed` }))
    return
  }

  // ... gestion GET/POST normales (voir code étape 2)
  res.writeHead(200, { "Content-Type": "application/json" })
  res.end(JSON.stringify({ status: "ok" }))
})

server.listen(3000)
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/inconnu
# → 404

curl -s -o /dev/null -w "%{http_code}" -X DELETE http://localhost:3000/api/ping
# → 405

Étape 4 — Servir des fichiers statiques

Ajoutez une route qui sert des fichiers since la page Système de fichiers :

import http from "node:http"
import type { IncomingMessage, ServerResponse } from "node:http"
import { readFile } from "node:fs/promises"
import { resolve, extname } from "node:path"

const MIME_TYPES = {
  ".html": "text/html",
  ".css": "text/css",
  ".js": "application/javascript",
  ".json": "application/json",
  ".png": "image/png",
  ".svg": "image/svg+xml",
}

async function servirFichierStatique(res: ServerResponse, cheminFichier: string) {
  try {
    const content = await readFile(cheminFichier)
    const ext = extname(cheminFichier)
    const mime = MIME_TYPES[ext] ?? "application/octet-stream"
    res.writeHead(200, { "Content-Type": mime })
    res.end(content)
  } catch {
    res.writeHead(404, { "Content-Type": "application/json" })
    res.end(JSON.stringify({ error: "Fichier non trouvé" }))
  }
}

const server = http.createServer(async (req: IncomingMessage, res: ServerResponse) => {
  const url = req.url.split("?")[0]

  if (url === "/") {
    await servirFichierStatique(res, resolve("public/index.html"))
    return
  }

  if (url.startsWith("/assets/")) {
    await servirFichierStatique(res, resolve(`.${url}`))
    return
  }

  res.writeHead(404, { "Content-Type": "application/json" })
  res.end(JSON.stringify({ error: "Not found" }))
})

server.listen(3000)

Récapitulatif de l'atelier

ÉtapeConcept appliquéPage source
1. Routes multiplesreq.url, req.method, conditionnementServeur HTTP natif
2. Parsing POSTAccumulator de chunks, JSON.parseServeur HTTP natif
3. 404 vs 405Codes HTTP précisServeur HTTP natif
4. Fichiers statiquesfs.readFile, types MIMESystème de fichiers

🎯 Exercice final de l'atelier

Étendez le serveur pour :

  1. Ajouter une route GET /api/items/:id qui renvoie un item par son ID (stocké en mémoire dans un tableau)
  2. Ajouter une route DELETE /api/items/:id qui supprime un item
  3. Utiliser URL pour parser proprement req.url (au lieu de split("?"))

→ Passez à l'exercice évalué quand vous êtes prêt.

Node.js

Terminez le quiz du chapitre pour le marquer comme complété.

Tableau de bord

On this page