Mini-défi : API de tâches complète
API CRUD complète testée au curl.
Objectifs
Assemblez tout le chapitre en une API CRUD cohérente, testée bout en bout avec curl — votre première vraie API REST.
🔗 Pour vous rafraîchir la mémoire : streams et pipe() · méthodes HTTP et codes de statut · en-têtes HTTP et JSON · curl pas à pas
Énoncé
Complétez votre serveur pour couvrir :
GET /tasks → liste (200)
POST /tasks → crée (201)
GET /tasks/:id → lit (200 / 404)
PATCH /tasks/:id → modifie partiellement (200 / 404)
DELETE /tasks/:id → supprime (204 / 404)
Contraintes :
- validation des entrées sur POST et PATCH (titre : chaîne non vide ≤ 200 car.) ;
- PATCH accepte
titreet/oufait(booléen) ; - codes exacts partout ; corps JSON explicites sur les erreurs.
Contraintes supplémentaires
- Le serveur doit démarrer sans erreur même avec un tableau vide.
- Aucune route ne doit laisser la requête sans réponse.
- Structurez : fonction par route (
listerTaches,creerTache, ...) appelée depuis un routeur lisible.
Indices
Sur la structure du routeur : un tableau de règles rend le code limpide :
const routes = [
{ methode: "GET", modele: /^\/tasks$/, handler: listerTaches },
{ methode: "POST", modele: /^\/tasks$/, handler: creerTache },
{ methode: "GET", modele: /^\/tasks\/(\d+)$/, handler: voirTache },
];
// puis : trouver la première règle dont méthode+modèle correspondent
const match = url.match(regexp); // match[1] = l'id capturé
Sur PATCH : fusionnez les champs fournis seulement :
if ("titre" in corps) tache.titre = corps.titre;
if ("fait" in corps) tache.fait = Boolean(corps.fait);
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Solution complète
import { createServer } from "node:http";
let taches = [];
let prochainId = 1;
function repondreJSON(reponse, statut, donnees) {
reponse.writeHead(statut, { "Content-Type": "application/json" });
reponse.end(JSON.stringify(donnees));
}
function lireCorps(requete) {
return new Promise(function (resoudre, rejeter) {
const morceaux = [];
requete.on("data", function (m) { morceaux.push(m); });
requete.on("end", function () {
resoudre(Buffer.concat(morceaux).toString("utf8"));
});
requete.on("error", rejeter);
});
}
async function listerTaches(req, res, params) {
repondreJSON(res, 200, taches);
}
async function creerTache(req, res) {
const brut = await lireCorps(req);
let corps;
try { corps = JSON.parse(brut); } catch {
return repondreJSON(res, 400, { erreur: "JSON invalide" });
}
if (typeof corps.titre !== "string" || !corps.titre.trim() || corps.titre.length > 200) {
return repondreJSON(res, 400, { erreur: "Titre requis (1-200 car.)" });
}
const tache = { id: prochainId++, titre: corps.titre.trim(), fait: false };
taches.push(tache);
repondreJSON(res, 201, tache);
}
function trouverToute(idTexte) {
const id = Number(idTexte);
return taches.find((t) => t.id === id);
}
async function voirTache(req, res, params) {
const tache = trouverToute(params[0]);
if (!tache) return repondreJSON(res, 404, { erreur: "Tâche inconnue" });
repondreJSON(res, 200, tache);
}
async function modifierTache(req, res, params) {
const tache = trouverToute(params[0]);
if (!tache) return repondreJSON(res, 404, { erreur: "Tâche inconnue" });
const brut = await lireCorps(req);
let corps;
try { corps = JSON.parse(brut); } catch {
return repondreJSON(res, 400, { erreur: "JSON invalide" });
}
if ("titre" in corps && (typeof corps.titre !== "string" || !corps.titre.trim())) {
return repondreJSON(res, 400, { erreur: "Titre invalide" });
}
if ("titre" in corps) tache.titre = corps.titre.trim();
if ("fait" in corps) tache.fait = Boolean(corps.fait);
repondreJSON(res, 200, tache);
}
async function supprimerTache(req, res, params) {
const index = taches.findIndex((t) => t.id === Number(params[0]));
if (index === -1) return repondreJSON(res, 404, { erreur: "Tâche inconnue" });
taches.splice(index, 1);
res.writeHead(204);
res.end();
}
const routes = [
{ methode: "GET", modele: /^\/tasks$/, handler: listerTacheS },
{ methode: "POST", modele: /^\/tasks$/, handler: creerTache },
{ methode: "GET", modele: /^\/tasks\/(\d+)$/, handler: voirTache },
{ methode: "PATCH", modele: /^\/tasks\/(\d+)$/, handler: modifierTache },
{ methode: "DELETE", modele: /^\/tasks\/(\d+)$/, handler: supprimerTache },
];
const serveur = createServer(async function (requete, reponse) {
const chemin = requete.url.toLowerCase().split("?")[0];
for (const route of routes) {
const match = route.modele.exec(chemin);
if (requete.method === route.methode && match) {
try {
return await route.handler(requete, reponse, match.slice(1));
} catch {
return repondreJSON(reponse, 500, { erreur: "Erreur interne" });
}
}
}
repondreJSON(reponse, 404, { erreur: "Route inconnue" });
});
serveur.listen(3000);