Connecter Node à PostgreSQL
Pool, requêtes $1/$2 et transactions via pg.
Objectifs
À la fin de cette leçon, vous saurez :
- installer et utiliser le driver pg ;
- exécuter des requêtes paramétrées depuis Node ;
- comprendre pourquoi on ne concatène jamais du SQL.
🔗 Pour vous rafraîchir la mémoire : CRUD en SQL · transactions SQL
Installer le driver
Le driver pg est la bibliothèque officielle de communication avec PostgreSQL :
npm init -y
npm install pg
Première requête
import { Pool } from "pg";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const resultat = await pool.query("SELECT * FROM tasks");
console.log(resultat.rows);
Le schéma complet :
Node (votre code)
↓ pool.query(...)
driver pg
↓ SQL sur connexion TCP
PostgreSQL
↓ lignes
resultat.rows → objets JavaScript prêts à l'emploi
Pool : un ensemble de connexions réutilisables. Ouvrir une connexion coûte cher ; le pool en maintient plusieurs ouvertes et les prêtent aux requêtes. Un seul Pool par application, créé au démarrage.
Les requêtes paramétrées : LA règle
Jamais ceci :
// ☠️ DANGER : injection SQL
const sql = "SELECT * FROM users WHERE nom = '" + nom + "'";
Si nom vaut ' OR '1'='1 — ou pire, une instruction destructive — l'attaquant exécute son SQL chez vous. Toujours ceci :
const resultat = await pool.query(
"SELECT * FROM tasks WHERE user_id = $1 AND fait = $2",
[userId, false]
);
Les $1, $2 sont des emplacements ; le tableau fournit les valeurs séparément. Le driver garantit que ces valeurs restent des données, jamais du code SQL exécutable. Ce point est si fondamental qu'il reviendra comme règle d'or dans tout le chapitre sécurité.
Insertion avec retour
const r = await pool.query(
"INSERT INTO tasks (titre, user_id) VALUES ($1, $2) RETURNING id, titre",
["Apprendre pg", userId]
);
console.log(r.rows[0].id); // l'id attribué par la base
RETURNING renvoie la ligne créée : plus besoin de re-interroger.
Transactions via le driver
const client = await pool.connect();
try {
await client.query("BEGIN");
await client.query("INSERT ...", [a]);
await client.query("UPDATE ...", [b]);
await client.query("COMMIT");
} catch (erreur) {
await client.query("ROLLBACK");
} finally {
client.release(); // rendre la connexion au pool, TOUJOURS
}
Pour une transaction, on garde UNE connexion (pas le pool), puis on la rend. Le finally avec release est non négociable : sans lui, le pool s'épuise.
Pourquoi pas un ORM tout de suite ?
Un ORM (chapitre 36) traduira vos objets en SQL. Mais un traducteur mal compris devient une boîte noire : sans savoir quel SQL il produit, vous ne saurez ni prédire une requête lente, ni écrire une jointure inhabituelle, ni comprendre un plan d'exécution.
C'est pourquoi ce cursus impose l'inverse : d'abord le SQL à la main, ici, avec le driver. Vous voyez chaque colonne demandée, chaque jointure écrite, chaque transaction explicitement ouverte et validée. Quand l'ORM arrivera, chaque appel repository.find() pourra être relu comme « le SELECT que j'aurais écrit moi-même » — et vous saurez vérifier, puisque vous aurez appris à regarder le SQL généré plutôt que le subir.
En résumé : le SQL manuel n'est pas une punition avant l'outil confortable, c'est ce qui rend l'outil contrôlable.
Exercice
- Connectez-vous à votre base et listez vos tables depuis Node.
- Écrivez
creerTache(titre, userId)paramétré qui renvoie la ligne créée. - Expliquez ce que ferait l'injection sur la version concaténée.
- Ajoutez une transaction qui crée un utilisateur ET sa première tâche.
Résumé
- Pool unique, connexions partagées ;
.rowspour lire les résultats. $1/$2+ tableau = requêtes sûres par construction.- Transaction = client dédié + BEGIN/COMMIT/ROLLBACK + release.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 3. Avec nom = "' DROP TABLE users; --", le SQL construit devient deux instructions dont une destructrice : le driver paramétré rend cette attaque impossible car la valeur n'est jamais interprétée comme SQL. C'est la différence entre donnée et code.