La todo list survit aux redémarrages
Migrer l’API vers la base avec un repository isolé.
Objectifs
À la fin de cette leçon, vous saurez :
- remplacer le tableau en mémoire par PostgreSQL ;
- mapper les routes REST sur des requêtes SQL paramétrées ;
- structurer un accès base isolé du reste du serveur.
🔗 Pour vous rafraîchir la mémoire : CRUD en SQL · anatomie d'une requête HTTP · méthodes HTTP et codes de statut
Le remplacement du stockage
Votre API du chapitre 19 garde tout dans let taches = []. Remplaçons par la base :
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
// au démarrage : s'assurer que la table existe
await pool.query(`
CREATE TABLE IF NOT EXISTS tasks (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
titre text NOT NULL,
fait boolean NOT NULL DEFAULT false,
cree_le timestamptz NOT NULL DEFAULT now()
)
`);
IF NOT EXISTS rend le démarrage répétable : la table se crée seulement si absente.
Les routes, une par une
// GET /tasks
const { rows } = await pool.query("SELECT * FROM tasks ORDER BY id");
repondreJSON(reponse, 200, rows);
// GET /tasks/:id
const { rows } = await pool.query("SELECT * FROM tasks WHERE id = $1", [id]);
if (!rows[0]) return repondreJSON(reponse, 404, { erreur: "Inconnue" });
repondreJSON(reponse, 200, rows[0]);
// POST /tasks
const { rows } = await pool.query(
"INSERT INTO tasks (titre) VALUES ($1) RETURNING *", [titre]);
repondreJSON(reponse, 201, rows[0]);
// PATCH /tasks/:id
const tache = await trouverToute(id);
if ("titre" in corps) await pool.query(
"UPDATE tasks SET titre = $1 WHERE id = $2", [corps.titre, id]);
if ("fait" in corps) await pool.query(
"UPDATE tasks SET fait = $1 WHERE id = $2", [Boolean(corps.fait), id]);
// DELETE /tasks/:id
const r = await pool.query("DELETE FROM tasks WHERE id = $1 RETURNING id", [id]);
if (!r.rows[0]) return repondreJSON(reponse, 404, { erreur: "Inconnue" });
reponse.writeHead(204);
Correspondance méthodes HTTP ↔ SQL — le mappage CRUD complet :
POST → INSERT GET → SELECT
PATCH → UPDATE DELETE → DELETE
Isoler l'accès à la base
Ne laissez pas pool.query(...) éparpillé dans le routeur. Regroupez :
// db.js : TOUT l'accès PostgreSQL vit ici
export function listerTaches() {
return pool.query("SELECT * FROM tasks ORDER BY id").then((r) => r.rows);
}
export function creerTache(titre) { ... }
export function trouverTache(id) { ... }
export function modifierTache(id, champs) { ... }
export function supprimerTache(id) { ... }
Le serveur appelle des fonctions nommées ; seul db.js connaît le SQL. Deux bénéfices immédiats :
- Testabilité : remplacer db.js par une fausse version pour tester le routeur.
- Évolutivité : changer de requêtes (ou d'ORM, chapitre 36) sans toucher au reste.
C'est votre premier repository — pattern que vous retrouverez partout, y compris dans Devmind lui-même.
Exercice
- Migrez votre API vers PostgreSQL avec ce découpage db.js.
- Redémarrez le serveur après avoir créé trois tâches : elles sont toujours là.
- Ajoutez le champ
user_id+ FK et la routeGET /users/:id/tasks. - Pourquoi le repository facilite-t-il les tests ?
Résumé
- CREATE TABLE IF NOT EXISTS au démarrage ; Pool unique.
- Une route REST ↔ une requête SQL paramétrée.
- Repository = isolation du SQL : testable et remplaçable.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 3.
SELECT t.* FROM tasks t WHERE t.user_id = $1 ORDER BY t.id;
Question 4. Le routeur peut être testé contre un faux repository (données fixes en mémoire) : pas besoin de base pour vérifier les statuts et validations. Inversement, db.js se teste contre une vraie base de test. Chaque couche a son terrain — c'est le principe d'isolation appliqué aux tests (chapitre 42).