Stephen Build ← Roadmap
0/0
Roadmap backend · Étape 2 sur 7

HTTP et Express ton premier serveur

Jusqu'ici, tu appelais le serveur des autres. Dans ce tuto, tu deviens le serveur. Tu vas comprendre ce qui voyage entre un navigateur et un backend, puis écrire avec Express une API qui sert tes tâches en JSON, et une page d'accueil.

Durée 1 semaine Niveau étape 1 faite Projet une API de tâches en lecture Mis à jour

À la fin, tu sauras

Les missions

Tout se passe dans ton dossier api-taches de l'étape 1. Garde deux terminaux ouverts : un où tourne le serveur, un pour tes commandes curl. Et lis ce que le serveur affiche, c'est lui qui te dit ce qui se passe.

Mission 0

À l'étape 4, ton appli météo appelait Open-Meteo avec fetch. Ce qui voyageait entre ton navigateur et leur serveur, c'est du HTTP : un format de messages tout simple, en texte. Le navigateur envoie une requête, le serveur renvoie une réponse. Toujours dans ce sens, toujours une réponse par requête.

curl est un petit programme qui fait des requêtes HTTP depuis le terminal, sans navigateur. Il est déjà installé sur Mac et sur Windows 10 ou plus récent. Ouvre le terminal de VS Code et tape :

Terminal
# La réponse du serveur : statut, en-têtes, puis le corps
$ curl -i "https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&current=temperature_2m,wind_speed_10m"

# Pareil, mais en montrant aussi la requête envoyée (lignes qui commencent par >)
$ curl -v "https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&current=temperature_2m,wind_speed_10m"

Voici ce que tu viens de voir, décortiqué :

Une requête, une réponse
LA REQUÊTE (ton ordi → le serveur)
  GET /v1/forecast?latitude=48.85&longitude=2.35&current=... HTTP/1.1   ← méthode, chemin, version
  Host: api.open-meteo.com                                             ← en-têtes : des infos en plus
  User-Agent: curl/8.7.1
  Accept: */*
                                                                       ← (pas de corps pour un GET)

LA RÉPONSE (le serveur → ton ordi)
  HTTP/1.1 200 OK                                                      ← le code de statut
  Content-Type: application/json; charset=utf-8                        ← en-têtes
  Date: Thu, 08 Oct 2026 06:34:35 GMT

  {"latitude":48.853382,"longitude":2.355208, ... }                    ← le corps
  • La méthode : ce que tu veux faire. GET = lire. Tu verras POST, PATCH et DELETE à l'étape 3.
  • L'URL : le serveur (api.open-meteo.com), le chemin (/v1/forecast) et, après le ?, des paramètres nom=valeur séparés par des &.
  • Les en-têtes : des infos en plus, une par ligne, sous la forme Nom: valeur. Content-Type dit ce que contient le corps : ici du JSON.
  • Le corps : les données elles-mêmes. Vide pour un GET, rempli dans la réponse.
  • Le code de statut : un nombre qui résume la réponse. 200 = tout va bien, 404 = introuvable. On les verra en détail en mission 6.

Tu peux voir la même chose dans le navigateur : ouvre ton appli météo, F12, onglet Réseau, clique sur la requête forecast : onglets En-têtes et Réponse.

Erreur fréquente

Sur Windows, dans PowerShell, curl -i affiche une erreur bizarre sur un paramètre : ton curl est un faux, un raccourci vers une autre commande. Tape curl.exe à la place de curl, partout dans ce tuto.

Demande à l'IA
Explique-moi ce qui se passe entre le moment où je tape une adresse dans mon navigateur et celui où la page s'affiche : la requête HTTP, le serveur, la réponse. Reste simple, je débute en backend.
Résultat attendu
Terminal
$ curl -i "https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&current=temperature_2m,wind_speed_10m"
HTTP/1.1 200 OK
Date: Thu, 08 Oct 2026 06:33:43 GMT
Content-Type: application/json; charset=utf-8
Transfer-Encoding: chunked
Connection: keep-alive

{"latitude":48.853382,"longitude":2.355208,"generationtime_ms":0.0985,"utc_offset_seconds":0,"timezone":"GMT","timezone_abbreviation":"GMT","elevation":39.0,"current_units":{"time":"iso8601","interval":"seconds","temperature_2m":"°C","wind_speed_10m":"km/h"},"current":{"time":"2026-10-08T06:30","interval":900,"temperature_2m":10.8,"wind_speed_10m":14.8}}

Les valeurs changent selon l'heure, mais la forme est toujours la même : une ligne de statut, des en-têtes, une ligne vide, puis le JSON. C'est exactement ce que ton fetch de l'étape 4 recevait.

Mission 1

Jusqu'ici, tu appelais le serveur des autres. Maintenant, c'est toi le serveur. Un serveur, c'est un programme qui tourne sans s'arrêter et qui attend des requêtes. À chaque requête, il appelle une fonction que tu as écrite, et cette fonction fabrique la réponse.

Node sait le faire tout seul, avec son module node:http. Dans ton dossier api-taches, crée hello-http.js :

hello-http.js
import { createServer } from "node:http";

const server = createServer((req, res) => {
  console.log(`Requête reçue : ${req.method} ${req.url}`);

  if (req.url === "/") {
    res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
    res.end("Bonjour depuis mon serveur !");
  } else {
    res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
    res.end("Page introuvable");
  }
});

server.listen(3000, () => {
  console.log("Serveur lancé sur http://localhost:3000");
});
Terminal
$ node hello-http.js

Ouvre http://localhost:3000 dans ton navigateur, puis http://localhost:3000/contact. Regarde le terminal à chaque fois.

  • createServer((req, res) => ...) : la fonction est appelée à chaque requête. req (request) contient la requête, res (response) sert à répondre.
  • req.method et req.url : la méthode et le chemin demandés. C'est avec eux que tu décides quoi répondre.
  • res.writeHead(200, ...) : le code de statut et les en-têtes. res.end(...) : le corps, et « j'ai fini ».
  • localhost : ton propre ordi. 3000 : le port, une sorte de numéro de porte. Plusieurs serveurs peuvent tourner sur ton ordi, chacun derrière sa porte.
  • Le serveur tourne jusqu'à ce que tu l'arrêtes : Ctrl + C dans le terminal. Pour taper d'autres commandes pendant ce temps, ouvre un deuxième terminal (le + du panneau Terminal de VS Code).
Erreur fréquente

Tu modifies hello-http.js, tu recharges la page, et rien ne change : Node a lu ton fichier au lancement, pas depuis. Arrête le serveur (Ctrl + C) et relance node hello-http.js. En mission 2, on automatise ça.

Résultat attendu
Terminal
$ node hello-http.js
Serveur lancé sur http://localhost:3000
Requête reçue : GET /
Requête reçue : GET /favicon.ico
Requête reçue : GET /contact
Navigateur
http://localhost:3000/          → Bonjour depuis mon serveur !
http://localhost:3000/contact   → Page introuvable
Deuxième terminal
$ curl -i http://localhost:3000/contact
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
Date: Thu, 08 Oct 2026 06:34:08 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

Page introuvable

Le GET /favicon.ico, c'est ton navigateur qui cherche tout seul l'icône de l'onglet. Le terminal reste bloqué tant que le serveur tourne : c'est normal, Ctrl + C l'arrête.

Mission 2

Avec node:http, chaque page demande des if sur req.url. Pour 20 routes, ça devient vite illisible. Express est le paquet npm le plus utilisé pour écrire des serveurs Node : il fait le tri des requêtes pour toi. Arrête hello-http.js (Ctrl + C) et installe-le :

Terminal
# Dans le dossier api-taches
$ npm install express

Crée server.js à la racine de api-taches :

server.js
import express from "express";

const app = express();
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.send("Bonjour depuis Express !");
});

app.listen(PORT, (error) => {
  if (error) throw error;
  console.log(`Serveur lancé sur http://localhost:${PORT}`);
});

Puis remplace les scripts de ton package.json. Le reste est déjà là depuis l'étape 1 et le npm install :

package.json
{
  "name": "api-taches",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "dev": "node --watch server.js",
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^5.2.1"
  }
}
Terminal
$ npm run dev
  • express() crée ton application, app. Tout passe par elle.
  • app.get("/", ...) : une route. « Quand quelqu'un fait un GET sur /, lance cette fonction. » Express s'occupe du tri, fini les if.
  • res.send(...) : envoie la réponse. Express met le statut 200 et le Content-Type tout seul.
  • process.env.PORT || 3000 : prend le port donné par l'hébergeur s'il y en a un (étape 7), sinon 3000.
  • if (error) throw error : si le serveur n'arrive pas à démarrer, tu vois l'erreur au lieu d'un faux « Serveur lancé ».
  • npm run dev lance node --watch server.js : le serveur redémarre tout seul à chaque sauvegarde. npm start, sans --watch, servira en production. Ta CLI marche toujours avec node cli.js.

hello-http.js a fait son travail : tu peux le supprimer.

Erreur fréquente

Error: listen EADDRINUSE: address already in use :::3000 : un autre serveur occupe déjà la porte 3000, souvent ton hello-http.js resté allumé dans un autre terminal. Trouve-le et fais Ctrl + C. Sinon, lance sur une autre porte : PORT=3001 npm run dev (Mac) ou $env:PORT=3001; npm run dev (PowerShell).

Demande à l'IA
Je débute en backend. Explique-moi ce qu'apporte Express par rapport au module node:http de Node.js, avec un exemple de code avant/après. Qu'est-ce qu'un framework, et pourquoi tout le monde n'écrit pas ses serveurs à la main ?
Résultat attendu
Terminal
$ npm install express

added 68 packages, and audited 69 packages in 2s

28 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

$ npm run dev

> api-taches@1.0.0 dev
> node --watch server.js

Serveur lancé sur http://localhost:3000
Restarting 'server.js'
Serveur lancé sur http://localhost:3000
Deuxième terminal
$ curl -i http://localhost:3000/
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: text/html; charset=utf-8
Content-Length: 24
ETag: W/"18-MfiAuKf5uh7RvNLpI2ZYs7CTcFA"
Date: Thu, 08 Oct 2026 06:34:35 GMT
Connection: keep-alive
Keep-Alive: timeout=5

Bonjour depuis Express !

Le « Restarting » apparaît dès que tu sauvegardes server.js : plus besoin de relancer à la main. Le numéro de version d'Express (ici 5.2.1) peut être un peu plus récent chez toi, tant qu'il commence par 5.

Mission 3

Une API, c'est un serveur qui répond avec des données (du JSON) plutôt qu'avec des pages. Ton storage.js de l'étape 1 sait déjà lire tasks.json : on le branche sur une route. Mets à jour server.js :

server.js
import express from "express";
import { loadTasks } from "./storage.js";

const app = express();
const PORT = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.send("Bonjour depuis Express !");
});

// Toutes les tâches, en JSON
app.get("/api/tasks", (req, res) => {
  res.json(loadTasks());
});

app.listen(PORT, (error) => {
  if (error) throw error;
  console.log(`Serveur lancé sur http://localhost:${PORT}`);
});

Le serveur a redémarré tout seul. Ajoute quelques tâches avec ta CLI, puis appelle l'API :

Terminal
# Dans un deuxième terminal : ajoute des tâches avec ta CLI de l'étape 1...
$ node cli.js add "Installer Node.js"
$ node cli.js add "Écrire mon premier serveur"
$ node cli.js add "Lire la doc d'Express"
# ... et coche la première avec la commande done de ta CLI


# ... puis demande-les à ton API
$ curl -i http://localhost:3000/api/tasks
  • import { loadTasks } from "./storage.js" : on réutilise ta fonction de l'étape 1, sans rien y changer.
  • res.json(...) : transforme le tableau en texte JSON (le JSON.stringify que tu connais) et met l'en-tête Content-Type: application/json. C'est ce que response.json() attend côté front.
  • Les routes de données commencent par /api/. Simple convention, mais elle évite de mélanger tes pages et tes données.
  • loadTasks() est appelé à chaque requête : ajoute une tâche avec la CLI, rappelle l'API, elle est là. Pas besoin de redémarrer.
  • La CLI et le serveur lisent le même tasks.json. Lance-les tous les deux depuis le dossier api-taches.
Erreur fréquente

Cannot find module '.../storage' : en ES modules, il faut le ./ au début et le .js à la fin. from "./storage.js", pas from "storage" ni from "./storage".

Résultat attendu
Réponse
$ curl -i http://localhost:3000/api/tasks
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 197
ETag: W/"c5-R3rUVLPrLvMWDjrMBo9ngWa6usc"
Date: Thu, 08 Oct 2026 06:34:27 GMT
Connection: keep-alive
Keep-Alive: timeout=5

[{"id":1759755600000,"text":"Installer Node.js","done":true},{"id":1759755660000,"text":"Écrire mon premier serveur","done":false},{"id":1759755720000,"text":"Lire la doc d'Express","done":false}]

Tes id et tes textes sont différents, mais tu retrouves ton fichier tasks.json, sur une seule ligne. Dans le navigateur, localhost:3000/api/tasks affiche la même chose (Chrome et Firefox ont une case « Impression élégante » pour le rendre lisible). Note le Content-Type: application/json : c'est res.json qui l'a mis.

Mission 4

Deux façons de passer des infos dans une URL :

  • Un paramètre de route, dans le chemin : /api/tasks/1759755660000. Pour désigner une chose précise.
  • Un paramètre de requête (query), après le ? : /api/tasks?done=true. Pour filtrer, trier, chercher. Facultatif.

Dans server.js, remplace ta route /api/tasks par ces deux routes :

server.js
// Toutes les tâches, ou seulement les faites / à faire avec ?done=
app.get("/api/tasks", (req, res) => {
  let tasks = loadTasks();

  if (req.query.done === "true") {
    tasks = tasks.filter((task) => task.done);
  } else if (req.query.done === "false") {
    tasks = tasks.filter((task) => !task.done);
  }

  res.json(tasks);
});

// Une seule tâche, par son id
app.get("/api/tasks/:id", (req, res) => {
  const id = Number(req.params.id);
  const task = loadTasks().find((t) => t.id === id);

  if (!task) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  res.json(task);
});
Deuxième terminal
$ curl http://localhost:3000/api/tasks/1759755660000
$ curl -i http://localhost:3000/api/tasks/42
$ curl "http://localhost:3000/api/tasks?done=true"
$ curl "http://localhost:3000/api/tasks?done=false"
  • /api/tasks/:id : le : fait de id une variable. Express range ce qu'il trouve à cet endroit dans req.params.id.
  • Number(...) : req.params.id est toujours du texte ("1759755660000"), alors que tes id sont des nombres. Sans la conversion, === ne trouve jamais rien.
  • res.status(404).json({ error: "Tâche introuvable" }) : on change le code de statut avant d'envoyer, pour que le front sache que ça n'a pas marché. Le return arrête la fonction : une seule réponse par requête.
  • req.query.done : ce qu'il y a après done=, lui aussi en texte. D'où la comparaison avec "true" entre guillemets.
  • let tasks : on part de toutes les tâches, et on filtre seulement si on nous le demande.
Erreur fréquente

Tu écris if (req.query.done === true) : le filtre ne marche jamais, tu reçois toujours tout. "true" (texte) et true (booléen) ne sont pas égaux avec ===. Tout ce qui arrive par l'URL est du texte.

À toi

Ajoute un paramètre ?limit= à GET /api/tasks : /api/tasks?limit=2 ne renvoie que les 2 premières tâches, et il se combine avec ?done=false&limit=1. Indice : Number(req.query.limit), la méthode slice des tableaux, et ne fais rien si limit est absent.

Demande à l'IA
Explique-moi la différence entre req.params et req.query dans Express, et comment choisir entre /api/tasks/12 et /api/tasks?id=12 quand je dessine les URL de mon API.
Résultat attendu
Réponses
$ curl http://localhost:3000/api/tasks/1759755660000
{"id":1759755660000,"text":"Écrire mon premier serveur","done":false}

$ curl -i http://localhost:3000/api/tasks/42
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 30
...

{"error":"Tâche introuvable"}

$ curl "http://localhost:3000/api/tasks?done=true"
[{"id":1759755600000,"text":"Installer Node.js","done":true}]

$ curl "http://localhost:3000/api/tasks?done=false"
[{"id":1759755660000,"text":"Écrire mon premier serveur","done":false},{"id":1759755720000,"text":"Lire la doc d'Express","done":false}]

Prends un vrai id de ton tasks.json pour le premier appel. Sans ?done=, ou avec une valeur bizarre comme ?done=peut-etre, tu reçois toutes les tâches.

Mission 5

Un middleware, c'est une fonction qui passe sur la requête avant tes routes. Les requêtes traversent une file de fonctions, dans l'ordre où tu les as écrites. Chacune peut regarder la requête, répondre elle-même, ou passer la main à la suivante avec next().

On en ajoute deux : un logger maison, qui note chaque requête dans le terminal, et express.static, qui sert les fichiers d'un dossier tels quels (HTML, CSS, images). Crée un dossier public avec ce index.html :

public/index.html
<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>API Tâches</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 640px; margin: 40px auto; padding: 0 16px; line-height: 1.5; }
  </style>
</head>
<body>
  <h1>API Tâches</h1>
  <p>Cette page est servie par mon serveur Express.</p>
  <ul>
    <li><a href="/api/tasks">/api/tasks</a> : toutes les tâches</li>
    <li><a href="/api/tasks?done=false">/api/tasks?done=false</a> : les tâches à faire</li>
    <li><a href="/api/tasks?done=true">/api/tasks?done=true</a> : les tâches faites</li>
  </ul>
</body>
</html>
Ton dossier
api-taches/
├── public/
│   └── index.html    # la page d'accueil, servie telle quelle
├── node_modules/
├── cli.js
├── package.json
├── package-lock.json
├── server.js
├── storage.js
├── tasks.json
└── .gitignore

Puis remplace tout server.js. La route app.get("/") disparaît : c'est public/index.html qui répond sur / maintenant.

server.js
import express from "express";
import { loadTasks } from "./storage.js";

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware 1 : affiche chaque requête dans le terminal
app.use((req, res, next) => {
  const heure = new Date().toLocaleTimeString("fr-FR");
  console.log(`[${heure}] ${req.method} ${req.url}`);
  next();
});

// Middleware 2 : sert les fichiers du dossier public
app.use(express.static("public"));

// Toutes les tâches, ou seulement les faites / à faire avec ?done=
app.get("/api/tasks", (req, res) => {
  let tasks = loadTasks();

  if (req.query.done === "true") {
    tasks = tasks.filter((task) => task.done);
  } else if (req.query.done === "false") {
    tasks = tasks.filter((task) => !task.done);
  }

  res.json(tasks);
});

// Une seule tâche, par son id
app.get("/api/tasks/:id", (req, res) => {
  const id = Number(req.params.id);
  const task = loadTasks().find((t) => t.id === id);

  if (!task) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  res.json(task);
});

app.listen(PORT, (error) => {
  if (error) throw error;
  console.log(`Serveur lancé sur http://localhost:${PORT}`);
});
  • app.use(...) : ajoute un middleware, pour toutes les requêtes, quelle que soit la méthode ou l'URL.
  • (req, res, next) : comme une route, avec next en plus. Appeler next(), c'est dire « j'ai fini, au suivant ».
  • express.static("public") : si un fichier du dossier public correspond à l'URL, il est envoyé. / donne public/index.html, /style.css donnerait public/style.css. Sinon, la requête continue vers tes routes.
  • L'ordre compte : le logger est en premier, donc il voit tout, même les fichiers statiques.
  • C'est dans ce public que ta vraie to-do list viendra s'installer à l'étape 5.
Erreur fréquente

Tu oublies next() dans ton logger : la requête s'affiche dans le terminal, puis plus rien. Le navigateur tourne dans le vide et curl attend pour toujours. Un middleware doit soit répondre, soit appeler next().

À toi

Améliore ton logger pour qu'il affiche aussi le code de statut et le temps de réponse : [08:34:35] GET /api/tasks/42 → 404 (3 ms). Indice : note Date.now() au début, et écoute la fin de la réponse avec res.on("finish", () => ...). Le statut est dans res.statusCode.

Demande à l'IA
Explique-moi ce qu'est un middleware dans Express, avec l'image d'une chaîne ou d'une file d'attente. Pourquoi l'ordre des app.use compte, et que se passe-t-il si j'oublie d'appeler next() ?
Résultat attendu
Terminal (npm run dev)
Serveur lancé sur http://localhost:3000
[08:34:27] GET /
[08:34:27] GET /favicon.ico
[08:34:31] GET /api/tasks?done=false
[08:34:35] GET /api/tasks/1759755660000
Navigateur
http://localhost:3000/   → la page « API Tâches » et ses trois liens
clic sur un lien         → le JSON des tâches correspondantes

Chaque requête laisse une ligne dans le terminal, y compris celles que tu n'as pas faites exprès (l'icône de l'onglet). Le texte « Bonjour depuis Express ! » a disparu : c'est public/index.html qui répond sur /.

Mission 6

Le code de statut, c'est la première chose que le front regarde : response.ok vaut true pour tous les codes de 200 à 299. Le premier chiffre donne la famille : 2xx ça a marché, 4xx le client s'est trompé, 5xx le serveur a planté. Ceux que tu vas utiliser dans tout le parcours :

  • 200 OK : tout va bien, voilà ta réponse. Le code par défaut d'Express.
  • 201 Created : j'ai créé ce que tu m'as envoyé (une nouvelle tâche, à l'étape 3).
  • 204 No Content : c'est fait, et je n'ai rien à te renvoyer (après une suppression). res.status(204).end().
  • 400 Bad Request : ta demande est mal formée (un texte vide, un id qui n'est pas un nombre).
  • 401 Unauthorized : il faut être connecté (étape 6).
  • 404 Not Found : ça n'existe pas, la tâche ou la route.
  • 500 Internal Server Error : le serveur a planté, ce n'est pas la faute du client. Express l'envoie tout seul quand ton code lève une erreur.

Essaie une route qui n'existe pas : curl -i http://localhost:3000/api/taches. Express répond 404, mais avec une page HTML, en plein milieu d'une API JSON. On ajoute un dernier middleware, après toutes les routes, qui attrape tout ce qui n'a pas trouvé preneur. Voici le server.js complet de l'étape :

server.js
import express from "express";
import { loadTasks } from "./storage.js";

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware 1 : affiche chaque requête dans le terminal
app.use((req, res, next) => {
  const heure = new Date().toLocaleTimeString("fr-FR");
  console.log(`[${heure}] ${req.method} ${req.url}`);
  next();
});

// Middleware 2 : sert les fichiers du dossier public
app.use(express.static("public"));

// Toutes les tâches, ou seulement les faites / à faire avec ?done=
app.get("/api/tasks", (req, res) => {
  let tasks = loadTasks();

  if (req.query.done === "true") {
    tasks = tasks.filter((task) => task.done);
  } else if (req.query.done === "false") {
    tasks = tasks.filter((task) => !task.done);
  }

  res.json(tasks);
});

// Une seule tâche, par son id
app.get("/api/tasks/:id", (req, res) => {
  const id = Number(req.params.id);
  const task = loadTasks().find((t) => t.id === id);

  if (!task) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  res.json(task);
});

// Aucune route n'a répondu : 404 en JSON (toujours en dernier)
app.use((req, res) => {
  res.status(404).json({ error: "Route introuvable" });
});

app.listen(PORT, (error) => {
  if (error) throw error;
  console.log(`Serveur lancé sur http://localhost:${PORT}`);
});
  • Le dernier app.use n'a pas de chemin : il prend tout. Une requête n'arrive jusqu'à lui que si aucun fichier statique ni aucune route n'a répondu avant.
  • Pas de next : il répond toujours, c'est le bout de la file.
  • Deux messages différents : « Tâche introuvable » (la route existe, la tâche non) et « Route introuvable » (l'URL ne veut rien dire). Ça aide beaucoup à déboguer.
  • Une erreur, c'est toujours un objet { error: "..." }. Le front pourra afficher data.error, quelle que soit la route.
Erreur fréquente

Tu mets la 404 tout en haut, juste après express() : plus rien ne marche, même /api/tasks répond « Route introuvable ». Les middlewares passent dans l'ordre, et celui-ci répond à tout sans appeler next(). Il doit rester juste avant app.listen.

À toi

Aujourd'hui, /api/tasks/abc répond 404 « Tâche introuvable ». Ce n'est pas tout à fait vrai : la demande elle-même n'a pas de sens. Fais-lui répondre 400 avec { error: "L'id doit être un nombre" }. Indice : Number("abc") donne NaN, et Number.isNaN(...) le détecte. Pense au return.

Résultat attendu
Terminal
# Avant la mission : la page d'erreur HTML d'Express
$ curl -i http://localhost:3000/api/taches
HTTP/1.1 404 Not Found
Content-Type: text/html; charset=utf-8
...
<pre>Cannot GET /api/taches</pre>

# Après : du JSON, comme le reste de ton API
$ curl -i http://localhost:3000/api/taches
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 29
ETag: W/"1d-xTVhWDoq1BSi9sgqC0p3XldDApo"
Date: Thu, 08 Oct 2026 06:34:28 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"Route introuvable"}

$ curl -i http://localhost:3000/style.css
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
...
{"error":"Route introuvable"}

La faute de frappe (taches au lieu de tasks) donne une erreur propre en JSON. Et /, /api/tasks et /api/tasks/:id marchent toujours comme avant.

Le projet

Une API de tâches en lecture

Ton api-taches devient un vrai serveur : une page d'accueil servie depuis public/, et une API qui expose tes tâches en JSON. Elle ne sait que lire pour l'instant : on ajoute, coche et supprime encore avec la CLI. Écrire depuis l'API, ce sera l'étape 3. Refais au moins les routes de la mission 4 sans regarder le code, et commite le tout avec Git (node_modules/ doit rester ignoré).

Ton projet est validé quand :

Bonus

Fais afficher par ta page d'accueil le nombre de tâches qu'il te reste : un petit script.js dans public/, un fetch("/api/tasks?done=false") et un .length. Pas besoin d'écrire localhost:3000 dans l'adresse : la page et l'API viennent du même serveur.

Pour aller plus loin

Étape 2 bouclée ?

Si ton serveur répond en JSON et que tu sais lire une réponse HTTP avec curl -i, t'as écrit ton premier backend. Prochaine étape : une API REST complète, pour créer, modifier et supprimer des tâches depuis l'API, sans passer par la CLI.

Étape 3 : Une API REST →