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

Une API REST complète

Ton serveur sait lire des tâches. Dans ce tuto, il apprend à créer, modifier et supprimer, à refuser les données bancales et à répondre proprement quand ça casse. À la fin, t'as une vraie API, celle que ta to-do list appellera à l'étape 5.

Durée 1 à 2 semaines Niveau débutant Projet l'API complète de ta to-do Mis à jour

À la fin, tu sauras

Les missions

Tu repars du projet api-taches de l'étape 2. Garde un terminal avec npm run dev ouvert tout le long, et teste chaque route dès qu'elle est écrite.

Mission 0

À l'étape 2, ton API savait lire. Pour une vraie to-do, il faut aussi créer, modifier et supprimer. Il existe une façon de ranger tout ça que presque toutes les API du web suivent : REST.

  • Une ressource, c'est une chose que ton API gère. Ici : les tâches. Elle a une adresse : /api/tasks pour la liste, /api/tasks/1759755600000 pour une tâche précise.
  • Un verbe, c'est la méthode HTTP. Elle dit ce que tu veux faire sur la ressource : GET lire, POST créer, PATCH modifier une partie, DELETE supprimer.

L'adresse dit quoi, le verbe dit quoi faire. Pas besoin d'adresses du genre /api/supprimer-tache?id=3. Voici le contrat de ton API à la fin de ce tuto :

Le contrat de l'API
Verbe    URL               Action                  Réponse si tout va bien
GET      /api/tasks        lister les tâches       200 + le tableau
GET      /api/tasks/:id    lire une tâche          200 + la tâche
POST     /api/tasks        créer une tâche         201 + la tâche créée
PATCH    /api/tasks/:id    modifier une tâche      200 + la tâche modifiée
DELETE   /api/tasks/:id    supprimer une tâche     204, sans corps
  • Deux adresses seulement, cinq actions. Le verbe fait la différence.
  • 201 Created : « c'est créé ». 204 No Content : « c'est fait, rien à te renvoyer ».
  • Les noms sont ceux de ta to-do du frontend : id, text, done. À l'étape 5, le front et le back se parleront sans traduction.

Lance ton serveur (npm run dev), puis dans un deuxième terminal, essaie de supprimer une tâche qui existe dans ton tasks.json :

Terminal
$ curl -i -X DELETE http://localhost:3000/api/tasks/1759755600000
Erreur fréquente

Confondre PATCH et PUT. PUT remplace la ressource entière : oublie text et la tâche le perd. PATCH ne change que ce que tu envoies. Pour cocher une case, c'est PATCH.

Demande à l'IA
Explique-moi ce qu'est une API REST avec une analogie de la vie de tous les jours. Pourquoi on met l'action dans la méthode HTTP (GET, POST, PATCH, DELETE) plutôt que dans l'URL ? N'écris pas de code.
Résultat attendu
Terminal
$ curl -i -X DELETE http://localhost:3000/api/tasks/1759755600000
HTTP/1.1 404 Not Found
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 29

{"error":"Route introuvable"}

La tâche existe, mais la route DELETE n'existe pas encore : ta 404 finale de l'étape 2 répond. Le verbe fait partie de l'adresse. À la fin de ce tuto, la même commande renverra 204.

Mission 1

Le navigateur ne sait envoyer que des GET quand tu tapes une adresse. Pour tester un POST ou un DELETE, il faut un outil. Le plus simple : une extension VS Code.

  1. Dans VS Code, onglet Extensions, cherche REST Client (l'auteur est Huachao Mao) et installe-la.
  2. À la racine de api-taches, crée un fichier requests.http.
  3. Au-dessus de chaque requête apparaît un petit lien Send Request. Clique dessus, serveur lancé.
requests.http
@base = http://localhost:3000

### Toutes les tâches
GET {{base}}/api/tasks

### Seulement les tâches faites
GET {{base}}/api/tasks?done=true

### Une seule tâche (mets un id qui existe dans ton tasks.json)
GET {{base}}/api/tasks/1759755600000
  • @base = ... : une variable. {{base}} est remplacé par l'adresse partout. Le jour où ton API est en ligne, tu changes une seule ligne.
  • ### sépare les requêtes. Le texte après sert de titre.
  • Une requête = le verbe, l'URL, puis (on le verra) les en-têtes et le corps.
  • Ce fichier, tu le gardes et tu le commites : c'est la liste de tout ce que ton API sait faire, prête à être rejouée.

Tu préfères le terminal ? curl marche aussi. -i affiche le statut et les en-têtes (sur Windows, tape curl.exe) :

Terminal
$ curl http://localhost:3000/api/tasks
$ curl -i http://localhost:3000/api/tasks/1759755600000
Erreur fréquente

« Connection refused » ou « ECONNREFUSED » : ton serveur ne tourne pas, ou sur un autre port. Garde un terminal avec npm run dev ouvert pendant tous tes tests, et vérifie le port affiché.

À toi

Ajoute deux requêtes : les tâches pas faites, et une tâche avec un id qui n'existe pas. Indice : la seconde doit répondre 404 avec le message de l'étape 2.

Résultat attendu
REST Client
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
...

[
  {
    "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
  }
]

La réponse s'ouvre dans un onglet à droite : le statut, les en-têtes, puis le JSON bien rangé. Tes tâches et tes id sont ceux de ton tasks.json.

Mission 2

Pour créer, le client envoie la tâche dans le corps de la requête, en JSON. Par défaut, Express ne lit pas ce corps : il faut lui ajouter le middleware express.json(). Voici ton server.js de l'étape 2, complet, avec les deux ajouts (le middleware et la route POST) :

server.js
import express from "express";
import { loadTasks, saveTasks } 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 : lit le corps JSON des requêtes et le range dans req.body
app.use(express.json());

// Middleware 3 : 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);
});

// Renvoie un message d'erreur si le texte n'est pas bon, sinon null
function checkText(text) {
  if (typeof text !== "string" || text.trim() === "") {
    return "Le texte est obligatoire";
  }
  if (text.trim().length > 200) {
    return "200 caractères maximum";
  }
  return null;
}

// Créer une tâche
app.post("/api/tasks", (req, res) => {
  const text = req.body?.text;
  const error = checkText(text);
  if (error) {
    return res.status(400).json({ error });
  }

  const tasks = loadTasks();
  const task = { id: Date.now(), text: text.trim(), done: false };
  tasks.push(task);
  saveTasks(tasks);
  res.status(201).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}`);
});
  • app.use(express.json()) : lit le corps JSON et le range dans req.body. Il doit être avant les routes.
  • req.body?.text : le ?. évite de planter si req.body est vide (undefined) : tu obtiens undefined au lieu d'une erreur.
  • checkText : on ne fait jamais confiance au client. Le texte doit être une chaîne (typeof), pas vide une fois les espaces enlevés (trim), et 200 caractères au plus.
  • Si c'est mauvais : 400 avec { error }, qui veut dire { error: error }. Le return arrête la fonction : rien n'est sauvegardé.
  • Si c'est bon : c'est le serveur qui choisit l'id et done: false, pas le client. Puis saveTasks, et 201 avec la tâche créée, pour que le client connaisse son id.

Ajoute ces requêtes à la fin de requests.http. Attention à la ligne vide entre les en-têtes et le corps, elle est obligatoire :

requests.http
### Créer une tâche
POST {{base}}/api/tasks
Content-Type: application/json

{
  "text": "Tester mon API"
}

### Erreur : texte vide
POST {{base}}/api/tasks
Content-Type: application/json

{
  "text": "   "
}

### Erreur : pas de texte du tout
POST {{base}}/api/tasks
Content-Type: application/json

{
  "done": true
}
Erreur fréquente

Toujours « Le texte est obligatoire », même avec un texte ? Il manque l'en-tête Content-Type: application/json dans ta requête (ou la ligne vide avant le corps) : express.json() ne lit que le JSON annoncé comme tel, et req.body reste vide. Autre coupable : app.use(express.json()) placé après tes routes.

Demande à l'IA
Pourquoi faut-il valider les données côté serveur dans une API, même si mon formulaire HTML vérifie déjà que le champ n'est pas vide ? Donne-moi un exemple concret de ce qui peut arriver sinon. N'écris pas de code.
Résultat attendu
Réponse : créer une tâche
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
...

{
  "id": 1791441312047,
  "text": "Tester mon API",
  "done": false
}
Réponse : texte vide
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
...

{
  "error": "Le texte est obligatoire"
}

Ouvre tasks.json : la nouvelle tâche est à la fin. Les deux requêtes en erreur n'ont rien ajouté. Dans le terminal du serveur, une ligne POST /api/tasks par envoi.

Mission 3

Cocher une tâche, corriger son texte : c'est une modification partielle, donc PATCH. Le client envoie seulement ce qui change : text, done, ou les deux. Ajoute cette route sous le POST, avant la 404 finale :

server.js
app.patch("/api/tasks/:id", (req, res) => {
  const tasks = loadTasks();
  const task = tasks.find((t) => t.id === Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  const { text, done } = req.body ?? {};
  if (text === undefined && done === undefined) {
    return res.status(400).json({ error: "Rien à modifier : envoie text et/ou done" });
  }
  if (text !== undefined) {
    const error = checkText(text);
    if (error) {
      return res.status(400).json({ error });
    }
  }
  if (done !== undefined && typeof done !== "boolean") {
    return res.status(400).json({ error: "done doit valoir true ou false" });
  }

  if (text !== undefined) task.text = text.trim();
  if (done !== undefined) task.done = done;
  saveTasks(tasks);
  res.json(task);
});
  • On cherche d'abord la tâche : si elle n'existe pas, 404. Inutile de valider le reste.
  • const { text, done } = req.body ?? {} : sort les deux champs du corps. ?? {} : si le corps est vide, on prend un objet vide à la place.
  • undefined veut dire « pas envoyé ». On ne valide et on ne modifie que ce qui a été envoyé. Rien du tout ? 400.
  • checkText resert : même règle qu'à la création. done doit être un vrai booléen (typeof done !== "boolean").
  • task pointe vers l'objet dans le tableau tasks : le modifier modifie le tableau, qu'on sauvegarde.
requests.http
### Cocher une tâche
PATCH {{base}}/api/tasks/1759755660000
Content-Type: application/json

{
  "done": true
}

### Renommer une tâche
PATCH {{base}}/api/tasks/1759755660000
Content-Type: application/json

{
  "text": "Écrire mon serveur Express"
}

### Erreur : done n'est pas un booléen
PATCH {{base}}/api/tasks/1759755660000
Content-Type: application/json

{
  "done": "oui"
}
Erreur fréquente

Envoyer "done": "true" (avec guillemets) au lieu de "done": true. Le premier est du texte, et le texte "false" est considéré comme vrai par JavaScript. C'est exactement pour ça qu'on vérifie le type : ton API répond 400 au lieu d'enregistrer n'importe quoi.

Résultat attendu
Réponse : cocher
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
...

{
  "id": 1759755660000,
  "text": "Écrire mon premier serveur",
  "done": true
}
Réponse : done à "oui"
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
...

{
  "error": "done doit valoir true ou false"
}

Après « Renommer », la tâche garde done: true : PATCH ne touche qu'à ce que tu envoies. Un id inconnu renvoie 404 avec Tâche introuvable.

Mission 4

La plus courte. Ajoute-la sous le PATCH, toujours avant la 404 :

server.js
app.delete("/api/tasks/:id", (req, res) => {
  const tasks = loadTasks();
  const index = tasks.findIndex((t) => t.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  tasks.splice(index, 1);
  saveTasks(tasks);
  res.status(204).end();
});
  • findIndex : la position de la tâche dans le tableau, ou -1 si elle n'y est pas.
  • splice(index, 1) : retire 1 élément à cette position.
  • res.status(204).end() : « c'est fait », sans corps. end() termine la réponse sans rien envoyer.
requests.http
### Supprimer une tâche
DELETE {{base}}/api/tasks/1759755600000

Envoie-la deux fois de suite et compare les réponses.

Astuce

Recopier les id à la main, c'est pénible. REST Client sait réutiliser la réponse d'une requête nommée avec # @name. Lance le POST, puis le DELETE :

requests.http
# @name nouvelle
POST {{base}}/api/tasks
Content-Type: application/json

{
  "text": "Tâche à supprimer"
}

### Supprime la tâche créée juste au-dessus
DELETE {{base}}/api/tasks/{{nouvelle.response.body.$.id}}
Erreur fréquente

Faire res.status(204).json(...) en pensant renvoyer un message : avec 204, le corps est toujours jeté, le client ne reçoit rien. Si tu veux renvoyer quelque chose, c'est 200. Et pas de .end() du tout ? La requête tourne dans le vide jusqu'à expirer.

Résultat attendu
Réponse : premier envoi
HTTP/1.1 204 No Content
X-Powered-By: Express
...
Réponse : deuxième envoi
HTTP/1.1 404 Not Found
Content-Type: application/json; charset=utf-8
...

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

Le premier envoi supprime, sans corps de réponse. Le deuxième ne trouve plus rien. Relance la commande curl de la mission 0 avec un id qui existe : elle répond 204 maintenant.

Mission 5

Ton server.js grossit. À l'étape 6, tu ajouteras les routes des comptes : tout dans un fichier, ça devient illisible. Express a un outil pour ranger : express.Router(), un mini-serveur qui ne contient que des routes. Crée un dossier routes et dedans tasks.js :

routes/tasks.js
import express from "express";
import { loadTasks, saveTasks } from "../storage.js";

const router = express.Router();

// Renvoie un message d'erreur si le texte n'est pas bon, sinon null
function checkText(text) {
  if (typeof text !== "string" || text.trim() === "") {
    return "Le texte est obligatoire";
  }
  if (text.trim().length > 200) {
    return "200 caractères maximum";
  }
  return null;
}

// GET /api/tasks (et /api/tasks?done=true)
router.get("/", (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);
});

// GET /api/tasks/:id
router.get("/: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);
});

// POST /api/tasks
router.post("/", (req, res) => {
  const text = req.body?.text;
  const error = checkText(text);
  if (error) {
    return res.status(400).json({ error });
  }

  const tasks = loadTasks();
  const task = { id: Date.now(), text: text.trim(), done: false };
  tasks.push(task);
  saveTasks(tasks);
  res.status(201).json(task);
});

// PATCH /api/tasks/:id
router.patch("/:id", (req, res) => {
  const tasks = loadTasks();
  const task = tasks.find((t) => t.id === Number(req.params.id));
  if (!task) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  const { text, done } = req.body ?? {};
  if (text === undefined && done === undefined) {
    return res.status(400).json({ error: "Rien à modifier : envoie text et/ou done" });
  }
  if (text !== undefined) {
    const error = checkText(text);
    if (error) {
      return res.status(400).json({ error });
    }
  }
  if (done !== undefined && typeof done !== "boolean") {
    return res.status(400).json({ error: "done doit valoir true ou false" });
  }

  if (text !== undefined) task.text = text.trim();
  if (done !== undefined) task.done = done;
  saveTasks(tasks);
  res.json(task);
});

// DELETE /api/tasks/:id
router.delete("/:id", (req, res) => {
  const tasks = loadTasks();
  const index = tasks.findIndex((t) => t.id === Number(req.params.id));
  if (index === -1) {
    return res.status(404).json({ error: "Tâche introuvable" });
  }

  tasks.splice(index, 1);
  saveTasks(tasks);
  res.status(204).end();
});

export default router;

Puis allège server.js : il ne garde que les middlewares, le montage et la 404.

server.js
import express from "express";
import tasksRouter from "./routes/tasks.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 : lit le corps JSON des requêtes et le range dans req.body
app.use(express.json());

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

// Toutes les routes des tâches, rangées dans routes/tasks.js
app.use("/api/tasks", tasksRouter);

// 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}`);
});
  • app.use("/api/tasks", tasksRouter) : monte le router sur ce préfixe. Dans le router, "/" veut dire /api/tasks et "/:id" veut dire /api/tasks/:id.
  • "../storage.js" : deux points, car routes/tasks.js est un dossier plus bas que storage.js.
  • export default router : le fichier exporte une seule chose, importée sans accolades dans server.js.
  • Les routes sont les mêmes qu'avant, on a juste remplacé app. par router. et retiré le préfixe.
Ton projet
api-taches/
├── package.json
├── server.js          # le serveur : middlewares, montage des routes, 404
├── routes/
│   └── tasks.js       # tout ce qui commence par /api/tasks
├── storage.js         # lire et écrire tasks.json (inchangé)
├── cli.js             # l'outil de l'étape 1, toujours là
├── tasks.json
├── requests.http      # tes requêtes de test
├── public/
│   └── index.html
└── .gitignore
Erreur fréquente

Garder le préfixe dans le router : router.get("/api/tasks", ...) répond en fait à /api/tasks/api/tasks, et toutes tes requêtes tombent sur « Route introuvable ». Autre classique : Cannot find module .../storage.js, c'est le ../ oublié.

À toi

Ajoute la recherche : GET /api/tasks?q=node ne renvoie que les tâches dont le texte contient « node », sans tenir compte des majuscules. Elle doit marcher avec ?done= en même temps. Indice : dans router.get("/"), un deuxième filter, toLowerCase() et includes().

Demande à l'IA
Explique-moi à quoi sert express.Router() dans Express et ce que fait app.use('/api/tasks', router). Comment Express décide quelle route répond à une requête ? N'écris pas mon code à ma place.
Résultat attendu
Terminal
$ npm run dev
Serveur lancé sur http://localhost:3000
[14:32:05] GET /api/tasks
[14:32:09] POST /api/tasks
[14:32:12] PATCH /api/tasks/1759755660000
[14:32:16] DELETE /api/tasks/1791441312047
[14:32:20] GET /api/n-importe-quoi

Relance toutes les requêtes de requests.http : mêmes réponses qu'avant le rangement. C'est le but : l'API n'a pas bougé, seul le code est mieux rangé. La dernière renvoie toujours 404 Route introuvable.

Mission 6

Ton API répond en JSON… sauf quand ça casse. Ajoute cette requête avec une virgule en trop, et envoie-la :

requests.http
### Erreur : JSON invalide (la virgule en trop)
POST {{base}}/api/tasks
Content-Type: application/json

{
  "text": "Oups",
}
Réponse avant la correction
HTTP/1.1 400 Bad Request
Content-Type: text/html; charset=utf-8
...

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Error</title>
</head>
<body>
<pre>SyntaxError: Expected double-quoted property name in JSON at position 20 (line 3 column 1)<br> &nbsp; &nbsp;at JSON.parse (&lt;anonymous&gt;)<br> &nbsp; &nbsp;at parse (/Users/alex/api-taches/node_modules/body-parser/...

Une page HTML en anglais, avec le chemin de tes fichiers : illisible pour le front, et ça montre l'intérieur de ton serveur. On ajoute un middleware d'erreur. Voici le server.js final de l'étape :

server.js
import express from "express";
import tasksRouter from "./routes/tasks.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 : lit le corps JSON des requêtes et le range dans req.body
app.use(express.json());

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

// Toutes les routes des tâches, rangées dans routes/tasks.js
app.use("/api/tasks", tasksRouter);

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

// Middleware d'erreur : 4 paramètres, toujours en dernier
app.use((err, req, res, next) => {
  if (err.type === "entity.parse.failed") {
    return res.status(400).json({ error: "JSON invalide" });
  }
  console.error(err);
  res.status(500).json({ error: "Erreur serveur" });
});

app.listen(PORT, (error) => {
  if (error) throw error;
  console.log(`Serveur lancé sur http://localhost:${PORT}`);
});
  • Un middleware avec 4 paramètres (err, req, res, next), Express le reconnaît comme gestionnaire d'erreurs. Il faut les 4, même si next ne sert pas.
  • Express l'appelle dès qu'une erreur est lancée dans une route ou un middleware. Il se place en dernier, après la 404.
  • err.type === "entity.parse.failed" : c'est express.json() qui n'a pas pu lire le JSON. C'est la faute du client : 400.
  • Tout le reste, c'est un bug chez toi : console.error(err) pour toi dans le terminal, 500 et un message neutre pour le client.

Pour tester la 500, casse exprès tasks.json (supprime la fin du fichier pour qu'il ressemble à ça), puis envoie GET {{base}}/api/tasks :

tasks.json
[
  { "id": 1759755600000, "text": "Installer Node.js",

Remets ensuite le fichier en état (Ctrl + Z dans VS Code).

Erreur fréquente

Ton middleware d'erreur n'est jamais appelé : il lui manque un paramètre (avec 3, Express le prend pour un middleware normal), ou il est placé avant app.use("/api/tasks", ...). L'ordre des app.use compte, comme l'ordre des lignes d'une recette.

À toi

Ajoute une route GET /api/stats qui renvoie { "total": 3, "done": 1, "todo": 2 } (le nombre de tâches, de faites, de pas faites). Indice : dans server.js, avant la 404, importe loadTasks, puis length et filter. Ajoute la requête à requests.http.

Demande à l'IA
Dans une API Express, quelle est la différence entre une erreur 400 et une erreur 500 ? Pourquoi on ne renvoie pas le détail de l'erreur (la stack trace) au client ? Explique-moi sans écrire mon code.
Résultat attendu
Réponse : JSON invalide
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
...

{
  "error": "JSON invalide"
}
Réponse : tasks.json cassé
HTTP/1.1 500 Internal Server Error
Content-Type: application/json; charset=utf-8
...

{
  "error": "Erreur serveur"
}
Terminal du serveur
[14:40:51] GET /api/tasks
SyntaxError: Expected double-quoted property name in JSON at position 56 (line 3 column 1)
    at JSON.parse (<anonymous>)
    at loadTasks (file:///Users/alex/api-taches/storage.js:7:15)
    at file:///Users/alex/api-taches/routes/tasks.js:19:15
    ...

Le visiteur reçoit un message propre, toi tu as le détail dans le terminal, avec le fichier et la ligne (storage.js:7). Le serveur ne s'arrête pas : répare tasks.json et la requête suivante repasse en 200.

Le projet

L'API complète de ta to-do

Termine l'API des tâches : les cinq actions du contrat, des réponses claires quand le client se trompe, et un fichier requests.http qui prouve que tout marche. Ensuite, fais un commit et pousse sur GitHub. Pour t'entraîner, réécris la route DELETE de mémoire, sans regarder la mission.

Ton projet est validé quand :

Bonus

Les id qui ne sont pas des nombres (/api/tasks/abc) tombent aujourd'hui en 404. Fais-leur répondre 400 « id invalide », à un seul endroit pour toutes les routes. Indice : cherche router.param dans la doc d'Express et Number.isInteger.

Pour aller plus loin

Étape 3 bouclée ?

Si ton fichier requests.http passe en entier sans surprise, t'as écrit ta première vraie API. Il lui manque une chose : un fichier JSON, ça casse vite dès que deux personnes écrivent en même temps. Prochaine étape : une vraie base de données, avec SQL.

Étape 4 : La base de données →