🔝 Retour au Sommaire
Node.js est une plateforme JavaScript côté serveur extrêmement populaire pour construire des APIs et applications web. Pour interagir avec PostgreSQL depuis Node.js, vous avez principalement deux approches :
- node-postgres (pg) : Driver natif bas niveau, contrôle total sur SQL
- Prisma : ORM moderne avec type-safety et génération de code
Ce tutoriel couvre les deux approches en profondeur pour vous permettre de choisir celle qui convient le mieux à votre projet.
Un driver (ou adaptateur) est une bibliothèque qui permet à votre code Node.js de communiquer directement avec PostgreSQL. Vous écrivez du SQL brut et récupérez les résultats.
Analogie : C'est comme parler directement au serveur PostgreSQL dans sa langue natale (SQL).
Un ORM (Object-Relational Mapping) est une couche d'abstraction qui traduit votre code JavaScript/TypeScript en requêtes SQL. Vous manipulez des objets JavaScript au lieu d'écrire du SQL.
Analogie : C'est comme avoir un traducteur automatique entre votre code et PostgreSQL.
| Caractéristique | node-postgres (pg) | Prisma |
|---|---|---|
| Type | Driver natif | ORM moderne |
| Langage | SQL brut | API JavaScript/TypeScript |
| Courbe d'apprentissage | Requiert connaissance SQL | Plus facile pour débutants |
| Contrôle | Total | Abstraction (moins de contrôle) |
| Type-safety | Non (sans effort) | Oui (TypeScript natif) |
| Migrations | Manuelles | Intégrées |
| Performances | Excellentes (direct SQL) | Très bonnes (optimisé) |
| Complexité SQL | Toutes requêtes possibles | Limité aux capacités de l'ORM |
| Taille bundle | Légère (~100 KB) | Plus lourde (~2 MB) |
Recommandation :
- node-postgres : Pour les projets nécessitant du SQL complexe, microservices légers, ou équipes expérimentées en SQL
- Prisma : Pour les projets TypeScript, équipes préférant la productivité, ou applications CRUD standard
node-postgres (npm package : pg) est le driver PostgreSQL le plus populaire pour Node.js. Il est :
- Léger : Peu de dépendances
- Mature : Utilisé en production depuis plus de 10 ans
- Flexible : Support de toutes les fonctionnalités PostgreSQL
- Performant : Accès direct sans overhead
npm install pgPour TypeScript (fortement recommandé) :
npm install --save-dev @types/node @types/pgconst { Client } = require('pg');
// Configuration de connexion
const client = new Client({
host: 'localhost',
port: 5432,
database: 'ma_database',
user: 'mon_utilisateur',
password: 'mon_password',
});
// Connexion asynchrone
async function connect() {
try {
await client.connect();
console.log('✅ Connecté à PostgreSQL');
// Votre code ici
} catch (error) {
console.error('❌ Erreur de connexion:', error);
} finally {
await client.end(); // Toujours fermer la connexion
}
}
connect();Important : Client crée une connexion unique. Pour les applications web, utilisez plutôt Pool (voir ci-dessous).
Créez un fichier .env :
DATABASE_URL=postgresql://user:password@localhost:5432/mydbPuis utilisez dotenv :
npm install dotenvrequire('dotenv').config();
const { Client } = require('pg');
const client = new Client({
connectionString: process.env.DATABASE_URL,
});En production, TOUJOURS utiliser un pool de connexions pour de meilleures performances :
const { Pool } = require('pg');
// Créer un pool global
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'ma_database',
user: 'mon_utilisateur',
password: 'mon_password',
max: 20, // Maximum 20 connexions
idleTimeoutMillis: 30000, // Fermer après 30s d'inactivité
connectionTimeoutMillis: 2000, // Timeout de connexion 2s
});
// Utilisation du pool
async function queryDatabase() {
try {
const result = await pool.query('SELECT NOW()');
console.log(result.rows[0]);
} catch (error) {
console.error('Erreur:', error);
}
}
// Fermer le pool à la fin de l'application
process.on('SIGTERM', async () => {
await pool.end();
});Avantages du Pool :
- Réutilisation des connexions (plus rapide)
- Gestion automatique des connexions
- Idéal pour serveurs web (Express, Fastify, etc.)
const { Pool } = require('pg');
const pool = new Pool(/* config */);
async function getUsers() {
try {
// Requête simple
const result = await pool.query('SELECT * FROM utilisateurs');
console.log(`Nombre de lignes : ${result.rowCount}`);
console.log('Colonnes :', result.fields.map(f => f.name));
console.log('Données :', result.rows);
// Parcourir les résultats
result.rows.forEach(user => {
console.log(`ID: ${user.id}, Nom: ${user.nom}, Email: ${user.email}`);
});
return result.rows;
} catch (error) {
console.error('Erreur SELECT:', error);
throw error;
}
}
getUsers();Structure du résultat :
{
rows: [
{ id: 1, nom: 'Alice', email: 'alice@example.com' },
{ id: 2, nom: 'Bob', email: 'bob@example.com' }
],
rowCount: 2,
fields: [/* métadonnées des colonnes */],
command: 'SELECT'
}// ❌ DANGEREUX - Injection SQL possible !
const email = "alice@example.com' OR '1'='1";
const query = `SELECT * FROM users WHERE email = '${email}'`;
await pool.query(query); ✅ TOUJOURS utiliser des paramètres :
// ✅ SÉCURISÉ - Paramètres échappés automatiquement
const email = 'alice@example.com';
const query = 'SELECT * FROM users WHERE email = $1';
const result = await pool.query(query, [email]); Syntaxe des paramètres :
$1,$2,$3, etc. : Positions des paramètres- Les valeurs sont passées dans un tableau :
[param1, param2, ...]
Exemples avec plusieurs paramètres :
// Paramètres multiples
const query = 'SELECT * FROM produits WHERE categorie = $1 AND prix > $2';
const result = await pool.query(query, ['Électronique', 100]);
// INSERT avec paramètres
const insertQuery = 'INSERT INTO users (nom, email, age) VALUES ($1, $2, $3) RETURNING id';
const values = ['Charlie', 'charlie@example.com', 35];
const result = await pool.query(insertQuery, values);
console.log('Nouvel ID:', result.rows[0].id); async function createUser(nom, email, age) {
const query = `
INSERT INTO utilisateurs (nom, email, age)
VALUES ($1, $2, $3)
RETURNING id, nom, email, created_at
`;
try {
const result = await pool.query(query, [nom, email, age]);
const newUser = result.rows[0];
console.log('✅ Utilisateur créé:', newUser);
return newUser;
} catch (error) {
// Violation de contrainte UNIQUE (email déjà existant)
if (error.code === '23505') {
throw new Error('Cet email existe déjà');
}
throw error;
}
}
// Utilisation
createUser('Alice', 'alice@example.com', 30);Insertion multiple (bulk insert) :
async function createMultipleUsers(users) {
// users = [['Alice', 'alice@...', 30], ['Bob', 'bob@...', 25]]
const query = `
INSERT INTO utilisateurs (nom, email, age)
VALUES ($1, $2, $3), ($4, $5, $6), ($7, $8, $9)
`;
// Aplatir le tableau
const values = users.flat();
const result = await pool.query(query, values);
console.log(`${result.rowCount} utilisateurs créés`);
}Meilleure approche pour bulk insert :
const format = require('pg-format'); // npm install pg-format
async function bulkInsertUsers(users) {
// users = [['Alice', 'alice@...', 30], ['Bob', 'bob@...', 25]]
const query = format(
'INSERT INTO utilisateurs (nom, email, age) VALUES %L RETURNING id',
users
);
const result = await pool.query(query);
return result.rows.map(r => r.id);
}// Récupérer un utilisateur par ID
async function getUserById(id) {
const query = 'SELECT * FROM utilisateurs WHERE id = $1';
const result = await pool.query(query, [id]);
if (result.rowCount === 0) {
return null; // Utilisateur non trouvé
}
return result.rows[0];
}
// Récupérer avec filtres multiples
async function searchUsers(filters) {
const { nom, ageMin, ageMax } = filters;
const query = `
SELECT * FROM utilisateurs
WHERE nom ILIKE $1
AND age BETWEEN $2 AND $3
ORDER BY nom
`;
const result = await pool.query(query, [`%${nom}%`, ageMin, ageMax]);
return result.rows;
}
// Utilisation
const users = await searchUsers({ nom: 'Ali', ageMin: 20, ageMax: 40 });async function updateUser(id, updates) {
const { nom, email, age } = updates;
const query = `
UPDATE utilisateurs
SET nom = $1, email = $2, age = $3, updated_at = NOW()
WHERE id = $4
RETURNING *
`;
const result = await pool.query(query, [nom, email, age, id]);
if (result.rowCount === 0) {
throw new Error('Utilisateur non trouvé');
}
return result.rows[0];
}
// Utilisation
const updated = await updateUser(1, {
nom: 'Alice Smith',
email: 'alice.smith@example.com',
age: 31
});UPDATE partiel (uniquement les champs fournis) :
async function partialUpdateUser(id, updates) {
const fields = [];
const values = [];
let paramCounter = 1;
// Construire dynamiquement la requête
Object.keys(updates).forEach(key => {
fields.push(`${key} = $${paramCounter}`);
values.push(updates[key]);
paramCounter++;
});
if (fields.length === 0) {
throw new Error('Aucun champ à mettre à jour');
}
const query = `
UPDATE utilisateurs
SET ${fields.join(', ')}, updated_at = NOW()
WHERE id = $${paramCounter}
RETURNING *
`;
values.push(id);
const result = await pool.query(query, values);
return result.rows[0];
}
// Utilisation : met à jour uniquement l'âge
await partialUpdateUser(1, { age: 32 });async function deleteUser(id) {
const query = 'DELETE FROM utilisateurs WHERE id = $1 RETURNING *';
const result = await pool.query(query, [id]);
if (result.rowCount === 0) {
throw new Error('Utilisateur non trouvé');
}
return result.rows[0];
}
// Suppression multiple
async function deleteUsersByAge(ageMax) {
const query = 'DELETE FROM utilisateurs WHERE age < $1';
const result = await pool.query(query, [ageMax]);
console.log(`${result.rowCount} utilisateurs supprimés`);
}async function transferMoney(fromAccountId, toAccountId, amount) {
// Récupérer une connexion du pool
const client = await pool.connect();
try {
// Démarrer la transaction
await client.query('BEGIN');
// Opération 1 : Débiter le compte source
await client.query(
'UPDATE comptes SET solde = solde - $1 WHERE id = $2',
[amount, fromAccountId]
);
// Opération 2 : Créditer le compte destination
await client.query(
'UPDATE comptes SET solde = solde + $1 WHERE id = $2',
[amount, toAccountId]
);
// Valider la transaction
await client.query('COMMIT');
console.log('✅ Transfert réussi');
} catch (error) {
// Annuler la transaction en cas d'erreur
await client.query('ROLLBACK');
console.error('❌ Transfert annulé:', error);
throw error;
} finally {
// Toujours libérer la connexion
client.release();
}
}Points clés :
await pool.connect(): Obtenir une connexion dédiéeBEGIN: Démarrer une transactionCOMMIT: Valider les changementsROLLBACK: Annuler les changementsclient.release(): Rendre la connexion au pool
async function complexTransaction() {
const client = await pool.connect();
try {
await client.query('BEGIN');
// Opération 1
await client.query('INSERT INTO logs (message) VALUES ($1)', ['Op 1']);
// Créer un point de sauvegarde
await client.query('SAVEPOINT mon_savepoint');
try {
// Opération 2 (risquée)
await client.query('INSERT INTO risky_table (data) VALUES ($1)', ['data']);
} catch (error) {
// Revenir au savepoint (annule uniquement l'Op 2)
await client.query('ROLLBACK TO SAVEPOINT mon_savepoint');
console.log('Savepoint restauré, Op 1 toujours valide');
}
// Opération 3
await client.query('INSERT INTO logs (message) VALUES ($1)', ['Op 3']);
await client.query('COMMIT');
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}// INTEGER, BIGINT
await pool.query('INSERT INTO produits (id, quantite) VALUES ($1, $2)', [1, 100]);
// NUMERIC/DECIMAL (utiliser des strings pour la précision)
await pool.query('INSERT INTO prix (montant) VALUES ($1)', ['19.99']);
// FLOAT/DOUBLE PRECISION
await pool.query('INSERT INTO mesures (valeur) VALUES ($1)', [3.14159]);// VARCHAR, TEXT
await pool.query('INSERT INTO articles (titre, contenu) VALUES ($1, $2)',
['Mon Titre', 'Contenu très long...']
);
// CHAR (taille fixe)
await pool.query('INSERT INTO codes (code_postal) VALUES ($1)', ['75001']);// DATE
const date = new Date('2025-11-23');
await pool.query('INSERT INTO evenements (date) VALUES ($1)', [date]);
// TIMESTAMP / TIMESTAMPTZ
const now = new Date();
await pool.query('INSERT INTO logs (timestamp) VALUES ($1)', [now]);
// INTERVAL
await pool.query("SELECT NOW() + INTERVAL '7 days'");Lecture des dates :
const result = await pool.query('SELECT created_at FROM users WHERE id = $1', [1]);
const date = result.rows[0].created_at; // Objet Date JavaScript
console.log(date.toISOString()); // "2025-11-23T10:30:00.000Z" // Insertion JSON
const profile = { nom: 'Alice', age: 30, tags: ['developer', 'postgresql'] };
await pool.query('INSERT INTO users (profile) VALUES ($1)', [JSON.stringify(profile)]);
// Ou directement (pg le sérialise automatiquement)
await pool.query('INSERT INTO users (profile) VALUES ($1)', [profile]);
// Lecture JSON
const result = await pool.query('SELECT profile FROM users WHERE id = $1', [1]);
const userProfile = result.rows[0].profile; // Objet JavaScript déjà parsé
console.log(userProfile.nom); // 'Alice' Requêtes JSONB avancées :
// Recherche dans JSONB
const result = await pool.query(
"SELECT * FROM users WHERE profile->>'nom' = $1",
['Alice']
);
// Extraction de chemin
const result = await pool.query(
"SELECT profile->'tags'->0 as premier_tag FROM users"
);// Insertion de tableau
const tags = ['javascript', 'postgresql', 'nodejs'];
await pool.query('INSERT INTO articles (tags) VALUES ($1)', [tags]);
// Lecture de tableau
const result = await pool.query('SELECT tags FROM articles WHERE id = $1', [1]);
const articleTags = result.rows[0].tags; // ['javascript', 'postgresql', 'nodejs']
// Recherche dans tableau
const result = await pool.query(
"SELECT * FROM articles WHERE $1 = ANY(tags)",
['postgresql']
);const { v4: uuidv4 } = require('uuid'); // npm install uuid
// Génération côté application (UUID v4)
const id = uuidv4();
await pool.query('INSERT INTO sessions (id, token) VALUES ($1, $2)', [id, 'token123']);
// Génération côté PostgreSQL (UUID v7 dans PG 18)
await pool.query('INSERT INTO events (id, data) VALUES (gen_uuid_v7(), $1)', ['event data']);
// Lecture UUID
const result = await pool.query('SELECT id FROM sessions WHERE token = $1', ['token123']);
const sessionId = result.rows[0].id; // String UUID Les prepared statements permettent de préparer une requête une fois et de l'exécuter plusieurs fois avec différents paramètres.
async function usePreparedStatement() {
const client = await pool.connect();
try {
// Préparer la requête
await client.query({
name: 'get-user-by-email',
text: 'SELECT * FROM users WHERE email = $1'
});
// Exécuter plusieurs fois (plus rapide)
const result1 = await client.query('get-user-by-email', ['alice@example.com']);
const result2 = await client.query('get-user-by-email', ['bob@example.com']);
// PostgreSQL réutilise le plan d'exécution
} finally {
client.release();
}
}Gain de performance : ~10-30% pour les requêtes répétées.
async function handleErrors() {
try {
await pool.query('INSERT INTO users (email) VALUES ($1)', ['duplicate@example.com']);
} catch (error) {
console.error('Code erreur:', error.code);
console.error('Message:', error.message);
console.error('Détail:', error.detail);
// Codes SQLSTATE courants
switch (error.code) {
case '23505': // Unique violation
console.error('Cette valeur existe déjà');
break;
case '23503': // Foreign key violation
console.error('Référence introuvable');
break;
case '23502': // NOT NULL violation
console.error('Champ obligatoire manquant');
break;
case '42P01': // Undefined table
console.error('Table inexistante');
break;
case '42703': // Undefined column
console.error('Colonne inexistante');
break;
default:
console.error('Erreur PostgreSQL:', error.message);
}
}
}async function queryWithRetry(query, params, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await pool.query(query, params);
} catch (error) {
if (attempt === maxRetries) {
throw error;
}
// Réessayer uniquement pour les erreurs de connexion
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT') {
console.log(`Tentative ${attempt}/${maxRetries} échouée, retry...`);
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
} else {
throw error;
}
}
}
}const express = require('express');
const { Pool } = require('pg');
const app = express();
app.use(express.json());
// Configuration du pool
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20,
});
// Middleware de gestion d'erreurs
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// GET /users - Liste des utilisateurs
app.get('/users', asyncHandler(async (req, res) => {
const result = await pool.query('SELECT id, nom, email FROM utilisateurs ORDER BY nom');
res.json(result.rows);
}));
// GET /users/:id - Un utilisateur
app.get('/users/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const result = await pool.query('SELECT * FROM utilisateurs WHERE id = $1', [id]);
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.json(result.rows[0]);
}));
// POST /users - Créer un utilisateur
app.post('/users', asyncHandler(async (req, res) => {
const { nom, email, age } = req.body;
const result = await pool.query(
'INSERT INTO utilisateurs (nom, email, age) VALUES ($1, $2, $3) RETURNING *',
[nom, email, age]
);
res.status(201).json(result.rows[0]);
}));
// PUT /users/:id - Modifier un utilisateur
app.put('/users/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const { nom, email, age } = req.body;
const result = await pool.query(
'UPDATE utilisateurs SET nom = $1, email = $2, age = $3 WHERE id = $4 RETURNING *',
[nom, email, age, id]
);
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.json(result.rows[0]);
}));
// DELETE /users/:id - Supprimer un utilisateur
app.delete('/users/:id', asyncHandler(async (req, res) => {
const { id } = req.params;
const result = await pool.query('DELETE FROM utilisateurs WHERE id = $1 RETURNING *', [id]);
if (result.rowCount === 0) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.json({ message: 'Utilisateur supprimé', user: result.rows[0] });
}));
// Middleware de gestion d'erreurs global
app.use((error, req, res, next) => {
console.error('Erreur serveur:', error);
if (error.code === '23505') {
return res.status(409).json({ error: 'Cette valeur existe déjà' });
}
res.status(500).json({ error: 'Erreur serveur interne' });
});
// Démarrage du serveur
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 Serveur démarré sur le port ${PORT}`);
});
// Fermeture propre
process.on('SIGTERM', async () => {
await pool.end();
process.exit(0);
});Prisma est un ORM next-generation pour Node.js et TypeScript. Il offre :
- Type-safety : Types TypeScript générés automatiquement
- Migrations : Gestion intégrée du schéma de base de données
- Prisma Studio : Interface graphique pour explorer vos données
- Prisma Client : API auto-générée pour vos requêtes
Votre Application (TypeScript/JavaScript)
↓
Prisma Client (généré)
↓
Prisma Query Engine
↓
PostgreSQL Database
Fichiers clés :
schema.prisma: Définition du schéma (single source of truth)prisma/migrations/: Historique des migrationsnode_modules/.prisma/client/: Client généré automatiquement
npm install prisma --save-dev
npm install @prisma/client npx prisma initCette commande crée :
prisma/schema.prisma: Fichier de configuration.env: Variables d'environnement
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// Définition du modèle (table)
model User {
id Int @id @default(autoincrement())
email String @unique
nom String
age Int? // ? = nullable
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
// Relation
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
// Relation many-to-one
author User @relation(fields: [authorId], references: [id])
}Fichier .env :
DATABASE_URL="postgresql://user:password@localhost:5432/mydb?schema=public"Vous définissez vos modèles dans schema.prisma.
npx prisma migrate dev --name initCette commande :
- Génère le SQL de migration
- Applique la migration à la base de données
- Régénère le Prisma Client
npx prisma generateGénère/régénère le Prisma Client avec vos types TypeScript.
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Votre code iciconst { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
// Créer un utilisateur
const user = await prisma.user.create({
data: {
email: 'alice@example.com',
nom: 'Alice',
age: 30,
},
});
console.log('Utilisateur créé:', user);
}
main()
.catch((e) => console.error(e))
.finally(async () => {
await prisma.$disconnect();
});Création avec relation :
// Créer un utilisateur ET ses posts en une seule requête
const userWithPosts = await prisma.user.create({
data: {
email: 'bob@example.com',
nom: 'Bob',
posts: {
create: [
{ title: 'Mon premier post', content: 'Hello World!' },
{ title: 'Mon deuxième post', content: 'Prisma est génial' },
],
},
},
include: {
posts: true, // Inclure les posts dans le résultat
},
});Création multiple (bulk insert) :
const users = await prisma.user.createMany({
data: [
{ email: 'user1@example.com', nom: 'User 1', age: 25 },
{ email: 'user2@example.com', nom: 'User 2', age: 30 },
{ email: 'user3@example.com', nom: 'User 3', age: 35 },
],
skipDuplicates: true, // Ignorer les doublons (email unique)
});
console.log(`${users.count} utilisateurs créés`);// Récupérer tous les utilisateurs
const allUsers = await prisma.user.findMany();
// Récupérer avec filtres
const filteredUsers = await prisma.user.findMany({
where: {
age: {
gte: 18, // Greater Than or Equal (age >= 18)
lte: 65, // Less Than or Equal (age <= 65)
},
},
orderBy: {
nom: 'asc', // Tri ascendant
},
take: 10, // LIMIT 10
skip: 0, // OFFSET 0 (pagination)
});
// Récupérer un seul utilisateur par ID
const user = await prisma.user.findUnique({
where: {
id: 1,
},
});
// Récupérer avec relations
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true, // Charger les posts associés
},
});
console.log('Utilisateur:', userWithPosts.nom);
console.log('Nombre de posts:', userWithPosts.posts.length); Recherche avancée :
// Opérateurs de filtrage
const users = await prisma.user.findMany({
where: {
OR: [
{ email: { contains: '@gmail.com' } }, // LIKE '%@gmail.com%'
{ nom: { startsWith: 'A' } }, // LIKE 'A%'
],
AND: [
{ age: { not: null } },
{ createdAt: { gte: new Date('2025-01-01') } },
],
},
});
// Recherche full-text (si configuré)
const results = await prisma.user.findMany({
where: {
nom: {
search: 'Alice Bob', // PostgreSQL full-text search
},
},
});// Mettre à jour un utilisateur
const updatedUser = await prisma.user.update({
where: {
id: 1,
},
data: {
age: 31,
email: 'newemail@example.com',
},
});
// Mettre à jour plusieurs enregistrements
const updateResult = await prisma.user.updateMany({
where: {
age: {
lt: 18, // Moins de 18 ans
},
},
data: {
age: 18, // Définir à 18
},
});
console.log(`${updateResult.count} utilisateurs mis à jour`);
// Incrémenter une valeur
const incrementedUser = await prisma.user.update({
where: { id: 1 },
data: {
age: {
increment: 1, // age = age + 1
},
},
});// Supprimer un utilisateur
const deletedUser = await prisma.user.delete({
where: {
id: 1,
},
});
// Supprimer plusieurs utilisateurs
const deleteResult = await prisma.user.deleteMany({
where: {
age: {
lt: 18,
},
},
});
console.log(`${deleteResult.count} utilisateurs supprimés`);
// Supprimer tous les enregistrements (attention !)
await prisma.user.deleteMany({});model User {
id Int @id @default(autoincrement())
nom String
posts Post[] // Un user a plusieurs posts
}
model Post {
id Int @id @default(autoincrement())
title String
authorId Int
author User @relation(fields: [authorId], references: [id])
}Utilisation :
// Créer un post pour un utilisateur existant
const post = await prisma.post.create({
data: {
title: 'Nouveau post',
author: {
connect: { id: 1 }, // Lier à l'utilisateur ID 1
},
},
});
// Récupérer un utilisateur avec ses posts
const userWithPosts = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: {
where: { published: true }, // Filtrer les posts
orderBy: { createdAt: 'desc' },
},
},
});model Post {
id Int @id @default(autoincrement())
title String
categories Category[] @relation("PostCategories")
}
model Category {
id Int @id @default(autoincrement())
name String
posts Post[] @relation("PostCategories")
}Prisma crée automatiquement la table de jonction _PostCategories.
Utilisation :
// Créer un post avec plusieurs catégories
const post = await prisma.post.create({
data: {
title: 'Mon article',
categories: {
connect: [
{ id: 1 },
{ id: 2 },
],
},
},
});
// Récupérer avec relations
const postWithCategories = await prisma.post.findUnique({
where: { id: 1 },
include: {
categories: true,
},
});// Transaction automatique avec $transaction
const [user, post] = await prisma.$transaction([
prisma.user.create({
data: { email: 'alice@example.com', nom: 'Alice' },
}),
prisma.post.create({
data: { title: 'Premier post', authorId: 1 },
}),
]);
// Si une opération échoue, tout est annulé (rollback)Pour des transactions complexes avec logique conditionnelle :
const result = await prisma.$transaction(async (tx) => {
// tx est un Prisma Client transactionnel
// Opération 1
const user = await tx.user.create({
data: { email: 'bob@example.com', nom: 'Bob' },
});
// Logique conditionnelle
if (user.id % 2 === 0) {
// Opération 2
await tx.post.create({
data: { title: 'Post pair', authorId: user.id },
});
}
return user;
});Parfois, vous avez besoin d'écrire du SQL brut pour des requêtes complexes :
// Requête brute avec typage
const users = await prisma.$queryRaw`
SELECT * FROM "User"
WHERE age > ${18}
ORDER BY nom
`;
console.log(users);
// Requête brute sans typage
const result = await prisma.$queryRawUnsafe(
'SELECT * FROM "User" WHERE email = $1',
'alice@example.com'
);
// Exécution SQL (INSERT, UPDATE, DELETE)
const affected = await prisma.$executeRaw`
UPDATE "User" SET age = age + 1 WHERE age < ${50}
`;
console.log(`${affected} lignes affectées`);npx prisma migrate dev --name add_user_profilenpx prisma migrate deploynpx prisma migrate resetWarning : Supprime toutes les données !
npx prisma migrate statusPrisma Studio est une interface web pour explorer et modifier vos données :
npx prisma studioOuvre une interface sur http://localhost:5555 où vous pouvez :
- Voir toutes vos tables
- Filtrer et rechercher
- Créer, modifier, supprimer des enregistrements
- Visualiser les relations
const express = require('express');
const { PrismaClient } = require('@prisma/client');
const app = express();
app.use(express.json());
const prisma = new PrismaClient();
// GET /users - Liste des utilisateurs
app.get('/users', async (req, res) => {
try {
const users = await prisma.user.findMany({
include: {
posts: {
where: { published: true },
},
},
});
res.json(users);
} catch (error) {
res.status(500).json({ error: 'Erreur serveur' });
}
});
// GET /users/:id - Un utilisateur
app.get('/users/:id', async (req, res) => {
try {
const user = await prisma.user.findUnique({
where: { id: parseInt(req.params.id) },
include: { posts: true },
});
if (!user) {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Erreur serveur' });
}
});
// POST /users - Créer un utilisateur
app.post('/users', async (req, res) => {
try {
const { email, nom, age } = req.body;
const user = await prisma.user.create({
data: { email, nom, age },
});
res.status(201).json(user);
} catch (error) {
if (error.code === 'P2002') {
return res.status(409).json({ error: 'Email déjà utilisé' });
}
res.status(500).json({ error: 'Erreur serveur' });
}
});
// PUT /users/:id - Modifier un utilisateur
app.put('/users/:id', async (req, res) => {
try {
const { email, nom, age } = req.body;
const user = await prisma.user.update({
where: { id: parseInt(req.params.id) },
data: { email, nom, age },
});
res.json(user);
} catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.status(500).json({ error: 'Erreur serveur' });
}
});
// DELETE /users/:id - Supprimer un utilisateur
app.delete('/users/:id', async (req, res) => {
try {
await prisma.user.delete({
where: { id: parseInt(req.params.id) },
});
res.json({ message: 'Utilisateur supprimé' });
} catch (error) {
if (error.code === 'P2025') {
return res.status(404).json({ error: 'Utilisateur non trouvé' });
}
res.status(500).json({ error: 'Erreur serveur' });
}
});
// Démarrage
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`🚀 Serveur démarré sur le port ${PORT}`);
});
// Fermeture propre
process.on('SIGTERM', async () => {
await prisma.$disconnect();
process.exit(0);
});| Code | Description | Action |
|---|---|---|
P2002 |
Contrainte unique violée | Retourner 409 Conflict |
P2003 |
Contrainte FK violée | Retourner 400 Bad Request |
P2025 |
Enregistrement non trouvé | Retourner 404 Not Found |
P1001 |
Impossible de se connecter | Vérifier la connexion DB |
P1017 |
Serveur fermé la connexion | Retry avec backoff |
✅ Choisir pg si :
- Vous avez besoin de requêtes SQL complexes (CTEs récursifs, window functions avancées)
- Vous optimisez pour des performances maximales
- Votre équipe est experte en SQL
- Vous avez des contraintes de taille (bundles légers)
- Vous travaillez sur des microservices simples
- Vous voulez un contrôle total sur le SQL généré
Exemple de cas d'usage :
- Système analytique avec requêtes complexes
- API haute performance avec latence critique
- Scripts de migration de données
- Applications legacy avec beaucoup de SQL existant
✅ Choisir Prisma si :
- Vous développez en TypeScript et voulez du type-safety
- Vous voulez de la productivité et moins de boilerplate
- Votre équipe est moins expérimentée en SQL
- Vous avez besoin de migrations automatiques
- Vous voulez Prisma Studio pour explorer vos données
- Vous construisez une application CRUD standard
Exemple de cas d'usage :
- SaaS moderne avec API REST/GraphQL
- Applications full-stack TypeScript (Next.js, Nest.js)
- Prototypes et MVPs rapides
- Équipes préférant l'abstraction
Benchmark indicatif (1000 requêtes SELECT simples) :
- node-postgres : ~150ms
- Prisma : ~180ms
Différence : ~20% plus lent pour Prisma, mais négligeable dans la plupart des applications.
Vous pouvez combiner les deux :
const { PrismaClient } = require('@prisma/client');
const { Pool } = require('pg');
const prisma = new PrismaClient();
const pool = new Pool(/* config */);
// Utiliser Prisma pour les opérations CRUD standard
const users = await prisma.user.findMany();
// Utiliser pg pour les requêtes complexes
const analytics = await pool.query(`
WITH monthly_stats AS (
SELECT
DATE_TRUNC('month', created_at) as month,
COUNT(*) as user_count
FROM "User"
GROUP BY month
)
SELECT * FROM monthly_stats
ORDER BY month DESC
LIMIT 12
`);node-postgres :
// ✅ Bon : Pool global
const pool = new Pool({ /* config */ });
// Export pour réutilisation
module.exports = { pool };
// ❌ Éviter : Créer un nouveau pool à chaque requêtePrisma :
// ✅ Bon : Instance globale unique
const { PrismaClient } = require('@prisma/client');
const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
global.prisma = prisma;
}
module.exports = { prisma };// .env
DATABASE_URL=postgresql://user:pass@localhost:5432/db
NODE_ENV=development
PORT=3000
// Utilisation
require('dotenv').config();
const config = {
database: process.env.DATABASE_URL,
port: process.env.PORT || 3000,
};node-postgres :
const pool = new Pool({
/* config */
log: (msg) => console.log('PostgreSQL:', msg),
});Prisma :
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
// Écouter les événements de requête
prisma.$on('query', (e) => {
console.log('Query: ' + e.query);
console.log('Duration: ' + e.duration + 'ms');
});// Express error handler
app.use((error, req, res, next) => {
console.error('Error:', error);
// Erreurs PostgreSQL (pg)
if (error.code === '23505') {
return res.status(409).json({ error: 'Duplicate entry' });
}
// Erreurs Prisma
if (error.code === 'P2002') {
return res.status(409).json({ error: 'Duplicate entry' });
}
res.status(500).json({ error: 'Internal server error' });
});Avec node-postgres :
const { Pool } = require('pg');
describe('User Repository', () => {
let pool;
beforeAll(async () => {
pool = new Pool({
connectionString: process.env.TEST_DATABASE_URL,
});
});
afterAll(async () => {
await pool.end();
});
it('should create a user', async () => {
const result = await pool.query(
'INSERT INTO users (email) VALUES ($1) RETURNING *',
['test@example.com']
);
expect(result.rows[0].email).toBe('test@example.com');
});
});Avec Prisma :
const { PrismaClient } = require('@prisma/client');
describe('User Repository', () => {
let prisma;
beforeAll(async () => {
prisma = new PrismaClient({
datasources: {
db: { url: process.env.TEST_DATABASE_URL },
},
});
});
afterAll(async () => {
await prisma.$disconnect();
});
it('should create a user', async () => {
const user = await prisma.user.create({
data: { email: 'test@example.com', nom: 'Test' },
});
expect(user.email).toBe('test@example.com');
});
});- Documentation officielle : https://node-postgres.com/
- GitHub : https://github.com/brianc/node-postgres
- npm : https://www.npmjs.com/package/pg
- Documentation officielle : https://www.prisma.io/docs
- GitHub : https://github.com/prisma/prisma
- Prisma Studio : Interface graphique intégrée
- Community Discord : https://pris.ly/discord
- Prisma Getting Started : https://www.prisma.io/docs/getting-started
- node-postgres Tutorial : https://node-postgres.com/guides/project-structure
- PostgreSQL + Node.js Best Practices : Nombreux articles sur Medium, Dev.to
- ✅ Driver natif léger et performant
- ✅ Utilisez toujours un
Poolen production - ✅ Paramètres (
$1,$2) pour éviter les injections SQL - ✅ Gestion manuelle des transactions avec
BEGIN/COMMIT/ROLLBACK - ✅ Idéal pour SQL complexe et performances critiques
- ✅ ORM moderne avec type-safety TypeScript
- ✅ Schéma unique (
schema.prisma) = source de vérité - ✅ Migrations automatiques et versionnées
- ✅ Prisma Studio pour explorer vos données visuellement
- ✅ API intuitive et productive pour CRUD
- ✅ Idéal pour développement rapide et maintenabilité
Pas de mauvais choix ! Les deux approches sont excellentes. Choisissez en fonction de :
- Votre expertise (SQL vs ORM)
- Vos besoins (performance vs productivité)
- Votre stack (JavaScript vs TypeScript)
- La complexité de vos requêtes
Conseil : Pour un nouveau projet TypeScript, commencez avec Prisma. Si vous avez besoin de SQL complexe, utilisez prisma.$queryRaw ou combinez avec node-postgres.
Prochaine étape : Explorez les autres drivers (Java JDBC, Go pgx, .NET Npgsql) pour comparer les approches et maîtriser PostgreSQL dans tous les écosystèmes.