1. fs.readFile et fs.writeFile (callbacks)

Le module fs (File System) est le module natif Node.js pour interagir avec le système de fichiers. Il propose une API asynchrone avec callbacks et une API synchrone (bloquante).

Lire un fichier — asynchrone (callback)

const fs = require('fs');

// Lecture asynchrone — non-bloquante
fs.readFile('config.txt', 'utf8', (err, contenu) => {
  if (err) {
    console.error('❌ Erreur lecture :', err.message);
    return;
  }
  console.log('Contenu :', contenu);
});

console.log('Cette ligne s\'affiche AVANT le contenu du fichier !');

Lire un fichier JSON

const fs = require('fs');

fs.readFile('./data/users.json', 'utf8', (err, data) => {
  if (err) { console.error('Fichier introuvable'); return; }
  try {
    const users = JSON.parse(data);
    console.log('Utilisateurs :', users.length);
    users.forEach(u => console.log(' -', u.name));
  } catch (parseErr) {
    console.error('JSON invalide :', parseErr.message);
  }
});

Écrire un fichier

const fs = require('fs');

// Écrire (remplace le fichier s'il existe)
const contenu = 'Ligne 1\nLigne 2\nLigne 3';
fs.writeFile('output.txt', contenu, 'utf8', (err) => {
  if (err) { console.error('Erreur écriture :', err); return; }
  console.log('✅ Fichier écrit !');
});

// Ajouter à un fichier existant (append)
fs.appendFile('logs.txt', '[' + new Date().toISOString() + '] Log entry\n', (err) => {
  if (err) console.error(err);
});

Version synchrone (bloquante)

const fs = require('fs');

// Synchrone — bloque le thread principal
// À utiliser uniquement pour les scripts (pas dans un serveur HTTP !)
try {
  const contenu = fs.readFileSync('config.json', 'utf8');
  const config = JSON.parse(contenu);
  console.log('Config chargée :', config);
} catch (err) {
  console.error('Erreur :', err.message);
}

// Écriture synchrone
fs.writeFileSync('output.txt', 'Hello synchrone !', 'utf8');
⚠️ Synchrone vs Asynchrone : Dans un serveur HTTP, utilisez TOUJOURS les versions asynchrones (fs.readFile). Les versions Sync bloquent le thread et paralysent toutes les requêtes entrantes.

2. fs.promises — async/await

L'API fs.promises expose les mêmes opérations sous forme de Promises, ce qui permet d'utiliser async/await pour un code lisible et sans callback hell.

Importer fs.promises

// Méthode 1 — déstructuration (Node.js >= 10)
const { promises: fs } = require('fs');

// Méthode 2 — module séparé (Node.js >= 14)
const fs = require('fs/promises');

// Méthode 3 — accès direct
const fsPromises = require('fs').promises;

Lire et modifier un fichier JSON

const fs = require('fs/promises');
const path = require('path');

async function mettreAJourConfig() {
  const fichier = path.join(__dirname, 'config.json');

  try {
    // Lire
    const contenu = await fs.readFile(fichier, 'utf8');
    const config = JSON.parse(contenu);

    // Modifier
    config.lastUpdate = new Date().toISOString();
    config.version = (parseFloat(config.version || '1.0') + 0.1).toFixed(1);

    // Réécrire
    await fs.writeFile(fichier, JSON.stringify(config, null, 2), 'utf8');
    console.log('✅ Config mise à jour :', config);

  } catch (err) {
    console.error('❌ Erreur :', err.message);
  }
}

mettreAJourConfig();

Opérations multiples en parallèle

const fs = require('fs/promises');

async function lirePlusieursConfigs() {
  try {
    // Lire plusieurs fichiers en PARALLÈLE avec Promise.all
    const [config, users, produits] = await Promise.all([
      fs.readFile('config.json',   'utf8'),
      fs.readFile('users.json',    'utf8'),
      fs.readFile('produits.json', 'utf8')
    ]);

    console.log('Config :', JSON.parse(config).name);
    console.log('Users  :', JSON.parse(users).length);
    console.log('Prods  :', JSON.parse(produits).length);

  } catch (err) {
    // Un seul catch pour toutes les erreurs
    console.error('Fichier manquant :', err.message);
  }
}
💡 Bonne pratique : Préférez fs/promises + async/await pour tout nouveau code Node.js. C'est plus lisible que les callbacks et plus simple que les Promises brutes.

3. Opérations sur les fichiers et dossiers

Au-delà de lire et écrire, fs permet de lister, créer, supprimer et déplacer des fichiers et dossiers.

readdir — Lister les fichiers

const fs = require('fs/promises');
const path = require('path');

async function listerFichiers(dossier) {
  try {
    const entrees = await fs.readdir(dossier, { withFileTypes: true });
    console.log('Contenu de', dossier + ' :');
    for (const entree of entrees) {
      const type = entree.isDirectory() ? '📁' : '📄';
      console.log('  ' + type, entree.name);
    }
  } catch (err) {
    console.error('Dossier introuvable :', err.message);
  }
}

listerFichiers('./data');

stat — Informations sur un fichier

const fs = require('fs/promises');

async function infoFichier(chemin) {
  const stat = await fs.stat(chemin);
  console.log('Chemin    :', chemin);
  console.log('Taille    :', stat.size, 'octets');
  console.log('Dossier ? :', stat.isDirectory());
  console.log('Fichier ? :', stat.isFile());
  console.log('Créé le   :', stat.birthtime.toLocaleString('fr-FR'));
  console.log('Modifié le:', stat.mtime.toLocaleString('fr-FR'));
}

infoFichier('package.json').catch(console.error);

mkdir, unlink, rename

const fs = require('fs/promises');

async function gestionFichiers() {
  // Créer un dossier (et ses parents si nécessaire)
  await fs.mkdir('./data/archives/2024', { recursive: true });
  console.log('✅ Dossier créé');

  // Écrire un fichier test
  await fs.writeFile('./data/test.txt', 'Contenu temporaire', 'utf8');

  // Renommer / déplacer
  await fs.rename('./data/test.txt', './data/archives/2024/test.txt');
  console.log('✅ Fichier déplacé');

  // Supprimer un fichier
  await fs.unlink('./data/archives/2024/test.txt');
  console.log('✅ Fichier supprimé');

  // Supprimer un dossier vide
  // await fs.rmdir('./data/archives/2024');
  // Supprimer dossier et son contenu (Node >= 14.14)
  // await fs.rm('./data/archives', { recursive: true });
}

gestionFichiers().catch(console.error);
ℹ️ Vérifiez l'existence d'un fichier avec fs.access(path) (throws si absent) ou fs.stat(path).then(...).catch(...). Évitez fs.existsSync() en production.

4. path et __dirname

La gestion des chemins est cruciale pour écrire du code portable (Windows/Linux/macOS). Node.js fournit path et __dirname pour ça.

__dirname vs process.cwd()

// __dirname = dossier du fichier courant (TOUJOURS absolu)
// process.cwd() = dossier depuis lequel node a été lancé

// Fichier : /projet/src/utils/fichier.js
console.log(__dirname);       // /projet/src/utils
console.log(process.cwd());   // /projet  (si lancé depuis /projet)

// BONNE PRATIQUE — utiliser __dirname pour les chemins relatifs au code
const configPath = path.join(__dirname, '..', 'config', 'app.json');
// → /projet/src/config/app.json  — correct peu importe d'où node est lancé

path — Méthodes essentielles

const path = require('path');

// join — Assembler des segments (cross-platform)
path.join('/home', 'user', 'docs', 'file.txt');
// → /home/user/docs/file.txt (Linux/Mac)
// → \home\user\docs\file.txt (Windows)

// resolve — Chemin absolu depuis CWD
path.resolve('config.json');  // /current/dir/config.json
path.resolve('..', 'shared'); // Remonter d'un niveau

// Décomposer un chemin
const p = '/home/alice/photo.jpg';
path.basename(p);        // 'photo.jpg'
path.basename(p, '.jpg');// 'photo'  (sans extension)
path.extname(p);         // '.jpg'
path.dirname(p);         // '/home/alice'

// Analyser un chemin complet
path.parse('/home/alice/photo.jpg');
// { root: '/', dir: '/home/alice', base: 'photo.jpg',
//   ext: '.jpg', name: 'photo' }

// Construire un chemin depuis ses parties
path.format({ dir: '/home/alice', name: 'photo', ext: '.jpg' });
// → '/home/alice/photo.jpg'

Exemple pratique : gestionnaire de config

const fs   = require('fs/promises');
const path = require('path');

// Toujours basé sur __dirname → portable
const CONFIG_DIR  = path.join(__dirname, 'config');
const CONFIG_FILE = path.join(CONFIG_DIR, 'app.json');

async function chargerConfig() {
  try {
    const data = await fs.readFile(CONFIG_FILE, 'utf8');
    return JSON.parse(data);
  } catch {
    // Retourner une config par défaut si le fichier n'existe pas
    return { port: 3000, env: 'development', debug: false };
  }
}

async function sauvegarderConfig(config) {
  await fs.mkdir(CONFIG_DIR, { recursive: true });
  await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
}

5. Streams et fs.watch

Les Streams permettent de lire ou écrire de grandes quantités de données par morceaux (chunks) sans charger tout en mémoire. fs.watch surveille les changements de fichiers en temps réel.

createReadStream — Lire par morceaux

const fs = require('fs');

// Lire un fichier volumineux morceau par morceau
const readable = fs.createReadStream('gros-fichier.csv', {
  encoding: 'utf8',
  highWaterMark: 64 * 1024  // Morceaux de 64 Ko
});

let totalChars = 0;
readable.on('data', (chunk) => {
  totalChars += chunk.length;
  process.stdout.write('.');  // Afficher un point par chunk
});
readable.on('end',   () => console.log('\n✅ Lu ! Total :', totalChars, 'caractères'));
readable.on('error', (err) => console.error('Erreur :', err.message));

createWriteStream — Écrire par morceaux

const fs = require('fs');

const writable = fs.createWriteStream('rapport.txt', { flags: 'w' });

// Écrire plusieurs morceaux
for (let i = 1; i <= 100; i++) {
  writable.write(`Ligne ${i}: données générées automatiquement\n`);
}

// Fermer le stream
writable.end(() => {
  console.log('✅ Fichier écrit avec le stream !');
});

.pipe() — Connecter des streams

const fs = require('fs');
const zlib = require('zlib'); // Module natif de compression

// Copier et compresser un fichier en une ligne !
fs.createReadStream('video.mp4')
  .pipe(zlib.createGzip())           // Compresser à la volée
  .pipe(fs.createWriteStream('video.mp4.gz'))
  .on('finish', () => console.log('✅ Fichier compressé !'));

// Copie simple sans transformation
fs.createReadStream('source.txt')
  .pipe(fs.createWriteStream('copie.txt'));

fs.watch — Surveiller les changements

const fs = require('fs');
const path = require('path');

const dossierSurveille = path.join(__dirname, 'data');

console.log('👀 Surveillance de :', dossierSurveille);
console.log('Modifiez des fichiers dans ce dossier...\n');

// Surveiller un dossier
const watcher = fs.watch(dossierSurveille, { recursive: true }, (event, filename) => {
  if (filename) {
    const timestamp = new Date().toLocaleTimeString('fr-FR');
    console.log(`[${timestamp}] Événement "${event}" sur : ${filename}`);
  }
});

// Arrêter après 30 secondes
setTimeout(() => {
  watcher.close();
  console.log('Surveillance arrêtée.');
}, 30000);

Quand utiliser les Streams ?

SituationApproche recommandée
Fichier < 10 Mofs.readFile / fs.promises
Fichier > 10 Mo (logs, CSV, vidéo)createReadStream
Copier un fichier volumineuxreadStream.pipe(writeStream)
Réponse HTTP volumineusereadStream.pipe(res)
Surveiller des changements en devfs.watch() ou chokidar
💡 Pour surveiller des fichiers en production ou avec des patterns glob, utilisez le package npm chokidar — il est plus fiable que fs.watch() sur tous les OS.
← N02 Modules & NPM Exercices →