N09 — Bases de Données avec Node.js
Persister les données au-delà du redémarrage : JSON files, SQLite et MongoDB.
1. Pourquoi une base de données ?
Limites des données en mémoire
Dans les modules précédents, toutes les données étaient stockées dans des tableaux JavaScript. Cette approche a des limites critiques en production :
- Perte au redémarrage — toutes les données disparaissent quand
node server.jss'arrête - Pas de partage — impossible avec plusieurs processus ou serveurs
- Pas de requêtes complexes — filtres, tris et jointures sont limités
- Mémoire vive — les données grossissent → la RAM se remplit
Choix selon la situation
| Situation | Solution recommandée |
|---|---|
| Apprentissage / prototype / < 1000 entrées | JsonDB (fichier JSON) |
| App locale / embarquée / petite API | SQLite (better-sqlite3) |
| API scalable / données flexibles / cloud | MongoDB (mongoose) |
2. JSON File DB — Classe JsonDB
La solution la plus simple pour apprendre la persistence sans base de données externe :
on lit/écrit un fichier .json sur le disque à chaque opération.
Construction de la classe
const fs = require('fs');
const path = require('path');
class JsonDB {
constructor(filename) {
// Chemin absolu vers le fichier JSON
this.filepath = path.join(__dirname, filename);
// Charger les données au démarrage
this.data = this._load();
}
// Charger depuis le disque (ou retourner {} en cas d'erreur)
_load() {
try {
return JSON.parse(fs.readFileSync(this.filepath, 'utf-8'));
} catch {
return {};
}
}
// Sauvegarder sur le disque
_save() {
fs.writeFileSync(this.filepath, JSON.stringify(this.data, null, 2));
}
// CRUD
findAll() { return this.data.items || []; }
findById(id) {
return this.findAll().find(item => item.id === parseInt(id));
}
insert(item) {
const all = this.findAll();
const ids = all.map(i => i.id);
item.id = ids.length ? Math.max(...ids) + 1 : 1;
item.createdAt = new Date().toISOString();
all.push(item);
this.data.items = all;
this._save();
return item;
}
update(id, data) {
const all = this.findAll();
const idx = all.findIndex(i => i.id === parseInt(id));
if (idx === -1) return null;
all[idx] = { ...all[idx], ...data, id: parseInt(id) };
this.data.items = all;
this._save();
return all[idx];
}
delete(id) {
const all = this.findAll();
const next = all.filter(i => i.id !== parseInt(id));
if (next.length === all.length) return false;
this.data.items = next;
this._save();
return true;
}
}
Principes clés :
readFileSync + JSON.parse pour lire,
JSON.stringify + writeFileSync pour écrire.
L'auto-increment utilise Math.max(...ids) + 1.
Le try/catch dans _load() gère le cas où le fichier n'existe pas encore.
3. Utiliser JsonDB avec Express
Instancier JsonDB une seule fois au démarrage et l'utiliser dans toutes les routes.
const express = require('express');
const JsonDB = require('./db'); // ou définir la classe inline
const app = express();
app.use(express.json());
const db = new JsonDB('articles.json');
// GET /articles — lister
app.get('/articles', (req, res) => {
res.json({ success: true, data: db.findAll() });
});
// GET /articles/:id
app.get('/articles/:id', (req, res) => {
const article = db.findById(req.params.id);
if (!article)
return res.status(404).json({ success: false, error: 'Article introuvable' });
res.json({ success: true, data: article });
});
// POST /articles
app.post('/articles', (req, res) => {
const { titre, auteur } = req.body;
if (!titre || !auteur)
return res.status(400).json({ success: false, error: 'titre et auteur requis' });
const article = db.insert({ titre, auteur });
res.status(201).json({ success: true, data: article });
});
// PUT /articles/:id
app.put('/articles/:id', (req, res) => {
const article = db.update(req.params.id, req.body);
if (!article)
return res.status(404).json({ success: false, error: 'Article introuvable' });
res.json({ success: true, data: article });
});
// DELETE /articles/:id
app.delete('/articles/:id', (req, res) => {
if (!db.delete(req.params.id))
return res.status(404).json({ success: false, error: 'Article introuvable' });
res.status(204).send();
});
app.listen(3009, () => console.log('🟢 API Articles — http://localhost:3009'));
4. Introduction à SQLite
Concepts SQLite
SQLite est un moteur de base de données relationnelle stocké dans un seul fichier .db.
Parfait pour les applications locales, les tests et les API légères.
Avec better-sqlite3, les requêtes sont synchrones (pas de callbacks/promesses).
// npm install better-sqlite3
const Database = require('better-sqlite3');
const db = new Database('app.db');
// Créer une table
db.exec(`
CREATE TABLE IF NOT EXISTS articles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
titre TEXT NOT NULL,
auteur TEXT NOT NULL,
createdAt TEXT DEFAULT (datetime('now'))
)
`);
// Insérer
const insert = db.prepare('INSERT INTO articles (titre, auteur) VALUES (?, ?)');
const info = insert.run('Node.js', 'Alice');
console.log('ID inséré :', info.lastInsertRowid);
// Lister
const all = db.prepare('SELECT * FROM articles').all();
console.log(all);
// Trouver par ID
const one = db.prepare('SELECT * FROM articles WHERE id = ?').get(1);
// Supprimer
db.prepare('DELETE FROM articles WHERE id = ?').run(1);
Quand utiliser SQLite ? Application desktop (Electron), API locale, tests d'intégration, données structurées avec relations (jointures), quand on veut du SQL sans serveur de base de données.
5. Introduction à MongoDB
MongoDB + Mongoose
MongoDB est une base de données NoSQL orientée document (JSON/BSON).
Mongoose est l'ODM (Object-Document Mapper) pour Node.js — il ajoute des schémas, la validation et des méthodes utilitaires.
// npm install mongoose
const mongoose = require('mongoose');
// Connexion
mongoose.connect('mongodb://localhost:27017/formation')
.then(() => console.log('MongoDB connecté'))
.catch(err => console.error(err));
// Schéma + Modèle
const articleSchema = new mongoose.Schema({
titre: { type: String, required: true, minLength: 3 },
auteur: { type: String, required: true },
contenu: String,
tags: [String],
createdAt: { type: Date, default: Date.now }
});
const Article = mongoose.model('Article', articleSchema);
// Créer
const article = await Article.create({ titre: 'Node.js', auteur: 'Alice' });
// Lister (async/await)
const articles = await Article.find({ auteur: 'Alice' }).sort('-createdAt').limit(10);
// Trouver par ID
const one = await Article.findById('64abc123...');
// Mettre à jour
await Article.findByIdAndUpdate('64abc123...', { titre: 'Nouveau titre' }, { new: true });
// Supprimer
await Article.findByIdAndDelete('64abc123...');
Quand utiliser MongoDB ? Données avec structure variable, applications cloud, API RESTful à grande échelle, schémas qui évoluent souvent, intégration facile avec des frontends JavaScript.
6. Bonnes pratiques
Validation avant sauvegarde
// ✅ Toujours valider AVANT d'écrire
app.post('/articles', (req, res) => {
const { titre, auteur } = req.body;
if (!titre || !auteur)
return res.status(400).json({
success: false,
error: 'Champs requis manquants'
});
// Seulement ici on écrit en base
const article = db.insert({ titre, auteur });
res.status(201).json({ success: true, data: article });
});
Gestion des erreurs fichier
// ✅ try/catch autour des I/O
_load() {
try {
return JSON.parse(
fs.readFileSync(this.filepath, 'utf-8')
);
} catch (err) {
// Fichier absent ou JSON corrompu
console.warn('Nouveau fichier DB créé');
return { items: [] };
}
}
Données de seed
// seed.js — peupler la BDD initiale
const db = new JsonDB('articles.json');
if (db.findAll().length === 0) {
['Node.js', 'Express', 'REST API']
.forEach((titre, i) => {
db.insert({
titre,
auteur: 'Formateur',
contenu: 'Contenu ' + (i + 1)
});
});
console.log('Seed terminé ✅');
}
Pagination systématique
// ✅ Toujours paginer les listes
app.get('/articles', (req, res) => {
const all = db.findAll();
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const start = (page - 1) * limit;
const data = all.slice(start, start + limit);
res.json({
success: true,
data,
total: all.length,
page,
limit
});
});