Ton JavaScript a toujours tourné dans un navigateur. Ici, tu le fais tourner directement sur ton ordi, avec Node.js. C'est la base de tout le parcours backend : à la fin, t'auras un gestionnaire de tâches en ligne de commande, qui garde ses données dans un fichier. Il deviendra l'API de ta to-do list.
Durée 3 à 5 joursNiveau parcours frontend, étapes 1 à 5Projet ta to-do list en ligne de commandeMis à jour
À la fin, tu sauras
Comprendre ce qu'est un backend, en lisant les requêtes d'un vrai site
Installer Node.js et lancer du JavaScript hors du navigateur
Lire ce que l'utilisateur tape dans le terminal avec process.argv
Découper ton code en modules avec import et export
Lire et écrire des fichiers JSON avec node:fs
Installer des paquets avec npm et créer tes propres scripts
Les missions
Fais-les dans l'ordre, dans le même dossier api-taches : chaque mission complète la précédente. Tape les commandes toi-même et lis ce que le terminal répond, même quand c'est rouge.
Mission 0
T'as déjà parlé à un backend sans le savoir. Ton appli météo de l'étape 4 demande les températures à Open-Meteo : ton navigateur est le client, et le programme qui tourne chez Open-Meteo et répond, c'est le serveur, le backend de quelqu'un d'autre. Dans ce parcours, tu passes de l'autre côté : c'est toi qui vas écrire ce programme.
Ouvre ton appli météo dans le navigateur (en ligne ou avec Live Server).
Ouvre les outils de développement (F12), onglet Réseau (Network), et clique sur le filtre Fetch/XHR.
Recharge la page (F5). Une ligne forecast?latitude=... apparaît : clique dessus.
Lis l'onglet En-têtes (Headers), puis l'onglet Réponse (Response).
Tu peux aussi coller l'adresse de la requête directement dans la barre d'adresse : le serveur te répond, sans page autour.
La méthodeGET : « donne-moi quelque chose ». Il en existe d'autres (envoyer, modifier, supprimer), tu les verras à l'étape 3.
L'adresse : le serveur (api.open-meteo.com), le chemin (/v1/forecast) et les paramètres après le ?.
Le code de statut200 : « tout va bien ». Un 404 voudrait dire « introuvable ».
La réponse : du JSON, que ton response.json() transforme en objet.
Le backend que tu vas construire fera pareil pour ta to-do list : recevoir des requêtes, garder les tâches, renvoyer du JSON. Mais d'abord, il faut faire tourner du JavaScript en dehors du navigateur.
Erreur fréquente
L'onglet Réseau est vide : il n'enregistre qu'une fois ouvert. Laisse-le ouvert et recharge la page. Tu vois des dizaines de lignes (images, CSS) ? Clique sur le filtre Fetch/XHR.
Demande à l'IA
Explique-moi la différence entre le frontend et le backend d'un site, avec l'exemple de mon appli météo qui appelle l'API Open-Meteo. Qu'est-ce qui tourne sur mon ordi, qu'est-ce qui tourne sur leur serveur, et qu'est-ce qui voyage entre les deux ?
Résultat attendu
Onglet Réseau, Headers
Request URL https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35¤t=temperature_2m,wind_speed_10m
Request Method GET
Status Code 200 OK
Content-Type application/json; charset=utf-8
Les chiffres changent selon l'heure, la forme reste la même. Tu sais maintenant dire ce que ton navigateur a demandé (GET + une adresse) et ce que le serveur a répondu (200 + du JSON).
Mission 1
Jusqu'ici, ton JavaScript tournait dans le navigateur. Node.js, c'est le même langage, mais qui tourne directement sur ton ordi (et plus tard sur un serveur). Pas de page, pas de document : à la place, il peut lire des fichiers, parler au réseau, attendre des requêtes. C'est ce qui fait un backend.
Va sur nodejs.org et télécharge la version LTS (« Long Term Support » : la version stable, maintenue longtemps). Il te faut Node 22 ou plus récent.
Lance l'installeur, garde les options par défaut. Si Node est déjà installé (étape 6 du frontend), vérifie juste la version.
Ferme et rouvre VS Code, puis ouvre son terminal : Terminal → Nouveau terminal.
Terminal
$ node --version
$ npm --version
# Lance le « REPL » : tu tapes du JS, Node répond tout de suite
$ node
Dans le REPL (le terminal affiche >), essaie quelques lignes, puis sors avec .exit ou Ctrl + C deux fois :
Le $ ne se tape pas : il marque le début d'une commande.
npm arrive avec Node : c'est l'outil qui installe des paquets de code écrits par d'autres (mission 6).
REPL : Read, Eval, Print, Loop. Il lit ta ligne, l'exécute, affiche le résultat, recommence. Comme la console du navigateur, mais sans page.
typeof document donne 'undefined' : la preuve qu'il n'y a pas de page HTML ici.
Erreur fréquente
« node n'est pas reconnu » ou « command not found » : le terminal a été ouvert avant l'installation. Ferme complètement VS Code et rouvre-le. Sur Windows, si PowerShell répond « l'exécution de scripts est désactivée sur ce système » pour npm, tape une fois Set-ExecutionPolicy -Scope CurrentUser RemoteSigned et réponds Oui.
Ton numéro de version peut être plus grand : l'important, c'est 22 ou plus. Le undefined après const est normal : une déclaration ne renvoie rien.
Mission 2
Le REPL, c'est pour tester. Pour un vrai programme, tu écris un fichier et tu le donnes à Node. Crée un dossier api-taches (pas dans mon-site : c'est un projet à part), ouvre-le dans VS Code, et crée hello.js :
hello.js
// Ton premier programme Node.js
console.log("Salut depuis Node.js !");
// process.argv : tout ce que t'as tapé après « node »
console.log(process.argv);
// Le 3e élément (index 2) : le premier mot après le nom du fichier
const name = process.argv[2] || "inconnu";
console.log(`Bonjour ${name}, on est le ${new Date().toLocaleDateString("fr-FR")}`);
Terminal
$ node hello.js Alex
$ node hello.js
node hello.js : Node lit le fichier et l'exécute de haut en bas, puis s'arrête. Le console.log écrit dans le terminal, plus dans la console du navigateur.
process : un objet fourni par Node qui décrit le programme en cours. Il n'existe pas dans le navigateur.
process.argv : un tableau avec tout ce que t'as tapé. Index 0 : le chemin de Node. Index 1 : ton fichier. À partir de 2 : tes mots à toi.
|| "inconnu" : si rien n'est tapé, process.argv[2] vaut undefined, donc on prend la valeur de secours.
Erreur fréquente
« Cannot find module '.../hello.js' » : le terminal n'est pas dans le bon dossier. Regarde le chemin affiché avant le $, et utilise cd api-taches pour y aller. ls (ou dir sur Windows) liste les fichiers du dossier où tu es.
À toi
Crée calcul.js : node calcul.js 3 4 doit afficher 3 + 4 = 7. Indice : tout ce qui sort de process.argv est du texte. Teste ce que donne "3" + "4" dans le REPL, et souviens-toi de Number().
Résultat attendu
Terminal
$ node hello.js Alex
Salut depuis Node.js !
[ '/usr/local/bin/node', '/Users/alex/api-taches/hello.js', 'Alex' ]
Bonjour Alex, on est le 08/10/2026
$ node hello.js
Salut depuis Node.js !
[ '/usr/local/bin/node', '/Users/alex/api-taches/hello.js' ]
Bonjour inconnu, on est le 08/10/2026
Les deux premiers éléments de process.argv sont toujours là (le chemin de Node, puis celui de ton fichier). Les chemins dépendent de ton ordi.
Mission 3
Un projet Node a une carte d'identité : le fichier package.json. Il dit comment s'appelle le projet, quels paquets il utilise, quelles commandes il propose. Dans le terminal, toujours dans api-taches :
Terminal
$ npm init -y
Ouvre le package.json créé et mets "type": "module" (remplace "commonjs" s'il est là, sinon ajoute la ligne, sans oublier la virgule avant) :
Maintenant, on découpe l'appli en deux fichiers : un qui s'occupe des données, un qui parle avec toi. Crée storage.js et cli.js :
storage.js
// Version provisoire : les tâches sont écrites en dur
const tasks = [
{ id: 1, text: "Installer Node.js", done: true },
{ id: 2, text: "Écrire mon premier module", done: false },
];
export function loadTasks() {
return tasks;
}
cli.js
import { loadTasks } from "./storage.js";
const tasks = loadTasks();
for (const task of tasks) {
const box = task.done ? "[x]" : "[ ]";
console.log(`${box} ${task.id} ${task.text}`);
}
Terminal
$ node cli.js
npm init -y : crée package.json en répondant « oui » à toutes les questions.
"type": "module" : dit à Node d'utiliser import et export, la syntaxe moderne, la même que dans le navigateur.
export devant une fonction : elle peut être utilisée par d'autres fichiers. Sans export, elle reste privée.
import { loadTasks } from "./storage.js" : le ./ veut dire « dans ce dossier », et l'extension .js est obligatoire.
Une tâche, c'est { id, text, done } : exactement le même objet que dans ta to-do list du frontend. C'est voulu, ton front parlera à ce backend à l'étape 5.
Erreur fréquente
« SyntaxError: Cannot use import statement outside a module » : le "type": "module" manque ou est mal écrit dans package.json. « ERR_MODULE_NOT_FOUND » : t'as écrit "./storage" sans le .js, ou oublié le ./.
Demande à l'IA
Explique-moi à quoi servent import et export en JavaScript, et pourquoi on découpe un programme en plusieurs fichiers. Pourquoi Node a besoin de "type": "module" dans package.json pour les accepter ?
Les tâches viennent de storage.js, et c'est cli.js qui les affiche. Deux fichiers, une seule appli.
Mission 4
Au frontend, tes tâches survivaient grâce à localStorage. Côté serveur, pas de navigateur, donc pas de localStorage. À la place, Node peut écrire dans de vrais fichiers. On range les tâches dans tasks.json. Remplace storage.js :
storage.js
import { readFileSync, writeFileSync, existsSync } from "node:fs";
const FILE = "tasks.json";
export function loadTasks() {
if (!existsSync(FILE)) return [];
return JSON.parse(readFileSync(FILE, "utf8"));
}
export function saveTasks(tasks) {
writeFileSync(FILE, JSON.stringify(tasks, null, 2));
}
Puis cli.js, qui comprend maintenant deux commandes, add et list :
cli.js
import { loadTasks, saveTasks } from "./storage.js";
// node cli.js add Acheter du pain → command = "add", args = ["Acheter", "du", "pain"]
const [command, ...args] = process.argv.slice(2);
const tasks = loadTasks();
if (command === "add") {
const text = args.join(" ").trim();
if (text === "") {
console.log('Écris le texte de la tâche : node cli.js add "Acheter du pain"');
} else {
const task = { id: Date.now(), text: text, done: false };
tasks.push(task);
saveTasks(tasks);
console.log(`Ajoutée : ${task.text} (id ${task.id})`);
}
} else if (command === "list") {
if (tasks.length === 0) {
console.log("Aucune tâche pour l'instant.");
}
for (const task of tasks) {
const box = task.done ? "[x]" : "[ ]";
console.log(`${box} ${task.id} ${task.text}`);
}
} else {
console.log("Commandes : add, list");
}
Terminal
$ node cli.js list
$ node cli.js add "Acheter du pain"
$ node cli.js add Réviser les tableaux
$ node cli.js list
node:fs : le module « file system » fourni avec Node, rien à installer. Le préfixe node: dit « module intégré à Node ».
existsSync(FILE) : le fichier existe ? La première fois, non : on renvoie un tableau vide au lieu de planter.
readFileSync(FILE, "utf8") : lit le fichier et renvoie son texte. JSON.parse le transforme en tableau, comme avec localStorage.getItem.
JSON.stringify(tasks, null, 2) : le 2 indente le JSON, pour que tu puisses lire tasks.json à l'œil nu.
process.argv.slice(2) enlève les deux premiers éléments, et const [command, ...args] range le premier mot dans command et tous les autres dans le tableau args.
id: Date.now() : le nombre de millisecondes depuis 1970, comme au frontend. Un nombre différent à chaque ajout.
Erreur fréquente
« SyntaxError: Unexpected end of JSON input » : tasks.json existe mais il est vide, ou t'as cassé le JSON en le modifiant à la main (une virgule en trop, un guillemet oublié). Supprime-le ou corrige-le. Autre piège : tasks.json est créé dans le dossier où est ton terminal. Lance toujours tes commandes depuis api-taches.
À toi
Ouvre tasks.json dans VS Code, change le texte d'une tâche et passe un done à true, sauvegarde, puis relance node cli.js list. Ensuite, casse le JSON exprès (enlève un guillemet) et lis l'erreur : quelle ligne de storage.js est citée, et pourquoi ?
Résultat attendu
Terminal
$ node cli.js list
Aucune tâche pour l'instant.
$ node cli.js add "Acheter du pain"
Ajoutée : Acheter du pain (id 1791441260454)
$ node cli.js add Réviser les tableaux
Ajoutée : Réviser les tableaux (id 1791441260654)
$ node cli.js list
[ ] 1791441260454 Acheter du pain
[ ] 1791441260654 Réviser les tableaux
Ferme le terminal, rouvre-le, tape node cli.js list : les tâches sont toujours là. Elles vivent dans un fichier, plus dans la mémoire du programme.
Mission 5
Il manque deux commandes pour avoir une vraie to-do list : done pour cocher ou décocher, remove pour supprimer. Les deux reçoivent un id. Voici le cli.js complet :
cli.js
import { loadTasks, saveTasks } from "./storage.js";
const [command, ...args] = process.argv.slice(2);
const tasks = loadTasks();
// Trouve une tâche à partir de l'id tapé ("1791441260454" → nombre)
function findTask(idText) {
const id = Number(idText);
return tasks.find((task) => task.id === id);
}
if (command === "add") {
const text = args.join(" ").trim();
if (text === "") {
console.log('Écris le texte de la tâche : node cli.js add "Acheter du pain"');
} else {
const task = { id: Date.now(), text: text, done: false };
tasks.push(task);
saveTasks(tasks);
console.log(`Ajoutée : ${task.text} (id ${task.id})`);
}
} else if (command === "list") {
if (tasks.length === 0) {
console.log("Aucune tâche pour l'instant.");
}
for (const task of tasks) {
const box = task.done ? "[x]" : "[ ]";
console.log(`${box} ${task.id} ${task.text}`);
}
} else if (command === "done") {
const task = findTask(args[0]);
if (!task) {
console.log("Tâche introuvable. Tape node cli.js list pour voir les id.");
} else {
task.done = !task.done;
saveTasks(tasks);
console.log(task.done ? `Faite : ${task.text}` : `À refaire : ${task.text}`);
}
} else if (command === "remove") {
const task = findTask(args[0]);
if (!task) {
console.log("Tâche introuvable. Tape node cli.js list pour voir les id.");
} else {
const rest = tasks.filter((t) => t.id !== task.id);
saveTasks(rest);
console.log(`Supprimée : ${task.text}`);
}
} else {
console.log(`Commandes :
node cli.js add "Texte" ajoute une tâche
node cli.js list affiche les tâches
node cli.js done <id> coche ou décoche une tâche
node cli.js remove <id> supprime une tâche`);
}
Terminal
# Recopie un id affiché par list (le tien est différent)
$ node cli.js list
$ node cli.js done 1791441260454
$ node cli.js list
$ node cli.js done 42
$ node cli.js remove 1791441260654
$ node cli.js
Number(idText) : ce qui vient du terminal est du texte. Sans la conversion, "1791441260454" === 1791441260454 est faux et on ne trouverait jamais la tâche.
tasks.find(...) : renvoie la première tâche qui correspond, ou undefined. D'où le if (!task).
task.done = !task.done : inverse la valeur. true devient false et l'inverse, comme une case qu'on coche et décoche.
filter garde toutes les tâches sauf celle à supprimer. On sauvegarde ce nouveau tableau.
Le dernier else affiche l'aide quand la commande est vide ou inconnue. Un bon programme explique comment s'en servir.
Erreur fréquente
Tu modifies la tâche mais list affiche encore l'ancienne version : t'as oublié saveTasks(tasks). Changer le tableau en mémoire ne suffit pas, il disparaît à la fin du programme. Seul le fichier reste.
À toi
Ajoute une commande clear qui supprime d'un coup toutes les tâches faites, et affiche combien ont été supprimées. Indice : filter sur done, puis compare la longueur du tableau avant et après.
Demande à l'IA
Dans mon programme Node, je lis tout le fichier tasks.json, je modifie le tableau, puis je réécris tout le fichier à chaque commande. Explique-moi pourquoi ça marche pour moi tout seul, et quels problèmes ça poserait si deux personnes ajoutaient une tâche exactement en même temps.
Résultat attendu
Terminal
$ node cli.js done 1791441260454
Faite : Acheter du pain
$ node cli.js list
[x] 1791441260454 Acheter du pain
[ ] 1791441260654 Réviser les tableaux
$ node cli.js done 42
Tâche introuvable. Tape node cli.js list pour voir les id.
$ node cli.js remove 1791441260654
Supprimée : Réviser les tableaux
$ node cli.js
Commandes :
node cli.js add "Texte" ajoute une tâche
node cli.js list affiche les tâches
node cli.js done <id> coche ou décoche une tâche
node cli.js remove <id> supprime une tâche
Un id qui n'existe pas donne un message clair, pas une erreur rouge. Et tasks.json suit chaque changement.
Mission 6
Un paquet, c'est du code écrit par quelqu'un d'autre, que tu installes au lieu de le réécrire. Il y en a des millions sur npmjs.com. On en installe un tout petit : picocolors, pour mettre de la couleur dans le terminal.
Terminal
$ npm install picocolors
Trois choses apparaissent : un dossier node_modules (le code du paquet), un fichier package-lock.json (les versions exactes installées), et une section dependencies dans package.json. Profites-en pour le ranger et ajouter un script start (supprime aussi hello.js, il a fait son temps) :
package.json
{
"name": "api-taches",
"version": "1.0.0",
"description": "Le backend de ma to-do list",
"type": "module",
"scripts": {
"start": "node cli.js"
},
"dependencies": {
"picocolors": "^1.1.1"
}
}
Puis colore cli.js :
cli.js
import pc from "picocolors";
import { loadTasks, saveTasks } from "./storage.js";
const [command, ...args] = process.argv.slice(2);
const tasks = loadTasks();
// Trouve une tâche à partir de l'id tapé ("1791441260454" → nombre)
function findTask(idText) {
const id = Number(idText);
return tasks.find((task) => task.id === id);
}
if (command === "add") {
const text = args.join(" ").trim();
if (text === "") {
console.log(pc.red('Écris le texte de la tâche : node cli.js add "Acheter du pain"'));
} else {
const task = { id: Date.now(), text: text, done: false };
tasks.push(task);
saveTasks(tasks);
console.log(pc.green(`Ajoutée : ${task.text}`) + pc.dim(` (id ${task.id})`));
}
} else if (command === "list") {
if (tasks.length === 0) {
console.log(pc.dim("Aucune tâche pour l'instant."));
}
for (const task of tasks) {
const box = task.done ? pc.green("[x]") : "[ ]";
const text = task.done ? pc.dim(task.text) : task.text;
console.log(`${box} ${pc.dim(task.id)} ${text}`);
}
} else if (command === "done") {
const task = findTask(args[0]);
if (!task) {
console.log(pc.red("Tâche introuvable. Tape node cli.js list pour voir les id."));
} else {
task.done = !task.done;
saveTasks(tasks);
console.log(task.done ? pc.green(`Faite : ${task.text}`) : `À refaire : ${task.text}`);
}
} else if (command === "remove") {
const task = findTask(args[0]);
if (!task) {
console.log(pc.red("Tâche introuvable. Tape node cli.js list pour voir les id."));
} else {
const rest = tasks.filter((t) => t.id !== task.id);
saveTasks(rest);
console.log(pc.yellow(`Supprimée : ${task.text}`));
}
} else {
console.log(`${pc.bold("Commandes :")}
node cli.js add "Texte" ajoute une tâche
node cli.js list affiche les tâches
node cli.js done <id> coche ou décoche une tâche
node cli.js remove <id> supprime une tâche`);
}
Terminal
# Lance le script "start" (ce qui suit -- est passé à cli.js)
$ npm start -- list
# Relance le programme tout seul à chaque sauvegarde d'un fichier .js
$ node --watch cli.js list
import pc from "picocolors" : pas de ./, donc Node va chercher le paquet dans node_modules.
pc.green(...), pc.dim(...) : entourent le texte de codes spéciaux que le terminal affiche en couleur.
"scripts" : des raccourcis. npm start lance node cli.js. Les autres noms se lancent avec npm run nom.
"^1.1.1" : « la version 1.1.1 ou une plus récente, tant qu'elle commence par 1 ».
node --watch : relance le fichier dès que tu sauvegardes du code. Pratique pour tester une modif sans retaper la commande. Ctrl + C pour l'arrêter.
node_modules peut peser des centaines de Mo et se recrée avec un simple npm install : il ne va jamais dans Git. Crée le .gitignore, puis sauvegarde le projet sur GitHub comme à l'étape 5 (un dépôt api-taches vide) :
.gitignore
node_modules/
Terminal
$ git init
$ git add .
$ git status
$ git commit -m "Gestionnaire de tâches en ligne de commande"
$ git remote add origin https://github.com/TON-PSEUDO/api-taches.git
$ git push -u origin main
package-lock.json, lui, va dans Git : il garantit que tout le monde installe les mêmes versions.
Quelqu'un clone ton dépôt ? Il tape npm install, et npm recrée node_modules à partir de package.json.
Erreur fréquente
« Cannot find package 'picocolors' » : t'as lancé npm install dans un autre dossier, ou supprimé node_modules. Retourne dans api-taches et relance npm install. Et si git status liste node_modules, ton .gitignore est mal nommé ou pas à la racine du projet.
À toi
Ajoute une commande stats qui affiche par exemple « 3 tâches : 1 faite, 2 à faire », avec les nombres en couleur. Indice : tasks.filter(...).length compte les tâches qui vérifient une condition.
Demande à l'IA
Explique-moi le rôle de package.json, package-lock.json et node_modules dans un projet Node. Pourquoi on met les deux premiers dans Git et pas le troisième ?
Résultat attendu
Terminal
$ npm install picocolors
added 1 package, and audited 2 packages in 329ms
found 0 vulnerabilities
$ npm start -- list
> api-taches@1.0.0 start
> node cli.js list
[x] 1791441260454 Acheter du pain
[ ] 1791441290229 Appeler Sam
Dans ton terminal, [x] et les messages « Ajoutée » sont en vert, les id et les tâches faites en gris. Sur GitHub, pas de node_modules.
Le projet
Ta to-do list en ligne de commande
Termine api-taches : un programme qui gère tes tâches depuis le terminal (add, list, done, remove) et les garde dans tasks.json. Ajoute les commandes clear et stats des encadrés « À toi », sans regarder le code des missions. Garde storage.js tel quel : il servira tel quel à ton serveur, aux étapes 2 et 3.
Ton projet est validé quand :
Bonus
Ajoute une commande edit <id> "nouveau texte", et refuse les textes de plus de 200 caractères avec un message clair (dans add aussi). C'est exactement la vérification que ton API fera à l'étape 3.
Le guide officiel de Node.js, en anglais. Les sections sur le terminal, les fichiers et npm complètent bien ce tuto.
La doc de npm, en anglais : package.json, les versions, les scripts.
Étape 1 bouclée ?
Si ton gestionnaire de tâches coche tous les critères, tu sais faire tourner du JavaScript hors du navigateur et garder des données dans un fichier. Prochaine étape : HTTP et Express, pour que tes tâches ne soient plus lues dans un terminal, mais demandées par un navigateur, comme Open-Meteo.