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.js s'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
  });
});
← Accueil Exercices →