Le module node fs de Node.js sert à lire, écrire, déplacer et supprimer des fichiers sans quitter l’écosystème JavaScript. En backend et côté API, il devient vite central dès qu’on gère des exports, des logs, des caches locaux, des fichiers temporaires ou des contenus envoyés par les utilisateurs. Je vais aller droit au but : comment l’utiliser proprement, quand préférer les promesses ou les streams, et quels pièges éviter en production.
Ce qu’il faut retenir avant de brancher fs à une route API
- `node:fs/promises` est le choix le plus sain dans la plupart des services backend modernes.
- `readFile()` et `writeFile()` sont simples, mais ils chargent le fichier entier en mémoire.
- Pour les fichiers volumineux, les streams évitent de bloquer inutilement l’application.
- Les versions synchrones sont utiles au démarrage, dans un script ou un outil CLI, pas dans une route appelée en boucle.
- La sécurité passe par des chemins validés, une gestion sérieuse des erreurs et des écritures atomiques.
- En environnement verrouillé, le mode `--permission` peut limiter l’accès disque même si le code est correct.
Ce que fait vraiment fs dans un backend
Je considère `fs` comme la couche la plus proche du disque. Il intervient quand l’application doit manipuler un fichier local, un dossier d’uploads, un répertoire temporaire ou un volume monté dans un conteneur. Il ne remplace ni une base de données ni un stockage objet, mais il reste indispensable pour tout ce qui touche au système de fichiers réel.
- Lecture : charger un JSON de configuration, un modèle de mail, une page statique ou un export déjà généré.
- Écriture : enregistrer un rapport, produire un cache, archiver une réponse ou conserver une trace d’audit.
- Déplacement : passer d’un fichier temporaire à un emplacement final après validation.
- Suppression : nettoyer les fichiers expirés, les uploads rejetés ou les artefacts de traitement.
- Inspection : vérifier la taille, la date de modification ou la nature d’un répertoire avant de traiter un flux.
Dans une API, ce module sert donc moins à “faire de l’I/O” en général qu’à relier une requête HTTP à un fichier exploitable. Une fois ce rôle clarifié, le vrai sujet devient le choix du bon mode d’appel.
Choisir entre callbacks, promesses et code synchrone
Le module existe sous trois formes, et le bon choix dépend surtout du contexte d’exécution. Dans une API moderne, je privilégie presque toujours les promesses, parce qu’elles s’enchaînent bien avec `async`/`await` et gardent le code lisible. Les callbacks restent utiles dans du code ancien, tandis que les fonctions synchrones gardent leur intérêt pour les scripts ou le démarrage d’un service.
| Mode | Quand je l’utilise | Point fort | Limite |
|---|---|---|---|
| Promesses | Routes API, services métier, traitements asynchrones | Lisible, facile à composer, naturel avec `async`/`await` | Demande une discipline propre sur les erreurs |
| Callbacks | Code legacy, intégrations anciennes, petits utilitaires | Très répandu historiquement, compatible partout | Se mélange mal avec des chaînes d’opérations longues |
| Synchrone | CLI, scripts d’administration, phase de boot, tests simples | Facile à lire quand l’opération est ponctuelle | Bloque l’event loop, donc pénalisant en serveur |
Dans un backend qui reçoit plusieurs requêtes en parallèle, le synchrone est vite un mauvais pari. Même si l’opération paraît courte, elle immobilise l’event loop le temps de finir la lecture ou l’écriture. Je le garde donc pour des tâches isolées, pas pour un point d’entrée HTTP fréquent. Une fois ce choix fait, la question suivante n’est plus l’API, mais la taille des fichiers que vous manipulez.

Lire et écrire sans saturer la mémoire
Pour un fichier petit ou moyen, `readFile()` et `writeFile()` restent les outils les plus simples. En pratique, je les réserve aux contenus qui restent modestes, souvent en dessous d’une dizaine de mégaoctets en production. Au-delà, ou dès qu’il peut y avoir plusieurs requêtes simultanées, je passe aux streams pour éviter de charger tout le contenu en mémoire.
Quand la simplicité suffit
import { readFile, writeFile, appendFile } from 'node:fs/promises';
const raw = await readFile('./config.json', 'utf8');
const config = JSON.parse(raw);
await writeFile('./cache/config.snapshot.json', JSON.stringify(config, null, 2), 'utf8');
await appendFile('./logs/audit.log', `${new Date().toISOString()} config chargé\n`, 'utf8');Ce type de code est parfait pour une configuration, un petit cache ou une trace d’audit. Le point à surveiller, c’est que le contenu entier passe en mémoire avant d’être rendu à l’appelant. Dès que le fichier grossit, la marge de confort disparaît vite.
Quand le flux devient le bon outil
import { createReadStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
const stream = createReadStream('./exports/rapport.csv');
await pipeline(stream, res);Le couple `createReadStream()` et `pipeline()` est bien plus robuste pour un téléchargement, un export CSV volumineux ou un fichier qui doit transiter vers une réponse HTTP. `pipeline()` m’intéresse particulièrement parce qu’il propage les erreurs proprement, sans me forcer à recoller plusieurs handlers à la main.
Quand on voit arriver des fichiers générés, téléchargés ou archivés, ce sont ces patterns qui font la différence entre un backend qui tient la charge et un autre qui sature pour rien.
Les usages que je croise le plus côté API
Dans un projet backend, je retrouve presque toujours les mêmes scénarios. Le module de fichiers n’est pas là pour faire joli dans l’architecture ; il sert à résoudre des besoins très concrets, souvent autour des données temporaires ou des sorties destinées à un utilisateur.
- Chargement de configuration : lire un fichier JSON, un template ou une table de routage au démarrage.
- Journalisation locale : écrire des traces d’audit ou des journaux techniques quand un système externe n’est pas encore branché.
- Exports : produire un CSV, un PDF ou un fichier de synthèse téléchargeable.
- Cache de travail : stocker temporairement un résultat coûteux à recalculer.
- Uploads et nettoyage : déplacer un fichier reçu, le valider, puis supprimer les artefacts inutiles.
Pour les exports ou les écritures critiques, j’évite d’écrire directement dans le fichier final. Je préfère passer par un fichier temporaire puis renommer, ce qui réduit le risque de laisser un fichier tronqué si le processus s’arrête au mauvais moment.
import { mkdir, rename, writeFile } from 'node:fs/promises';
import path from 'node:path';
const dir = path.resolve('tmp/exports');
const tmpFile = path.join(dir, 'rapport.csv.tmp');
const finalFile = path.join(dir, 'rapport.csv');
await mkdir(dir, { recursive: true });
await writeFile(tmpFile, csvContent, 'utf8');
await rename(tmpFile, finalFile);Cette séquence paraît banale, mais elle change vraiment la fiabilité d’une API qui génère des fichiers. Le prochain niveau de maturité consiste à verrouiller les chemins et à traiter les erreurs comme un cas normal, pas comme une exception rare.
Sécurité, chemins et erreurs à traiter dès le départ
Quand on manipule des fichiers depuis une API, le problème n’est pas seulement technique ; il est aussi sécuritaire. Un nom de fichier fourni par un client ne doit jamais être repris tel quel, et un simple oubli peut ouvrir la porte à une traversée de répertoire ou à une fuite de données.
Verrouiller les chemins
Je construis toujours les chemins avec `path.resolve()` ou `path.join()`, puis je vérifie qu’ils restent dans un répertoire autorisé. C’est une protection simple, mais elle évite beaucoup d’ennuis.
import path from 'node:path';
const baseDir = path.resolve('/var/app/uploads');
const candidate = path.resolve(baseDir, userProvidedName);
if (!candidate.startsWith(`${baseDir}${path.sep}`)) {
throw new Error('Chemin invalide');
}Je ne fais pas confiance à une chaîne venue du client, même si elle a l’air propre. Et je n’utilise pas `access()` comme une garantie absolue avant d’ouvrir un fichier : je préfère tenter l’opération puis gérer l’erreur, parce qu’entre le contrôle et l’action, l’état du disque peut déjà avoir changé.
Lire aussi : Validation e-mail PHP - La méthode qui marche vraiment
Mapper les erreurs proprement
- `ENOENT` : le fichier n’existe pas, donc une réponse HTTP 404 est souvent plus juste qu’une 500.
- `EACCES` ou `EPERM` : le processus n’a pas les droits, ce qui pointe vers un problème de configuration ou de sécurité.
- `ENOSPC` : l’espace disque est plein, un cas qu’il faut surveiller si l’application écrit beaucoup.
- `EISDIR` : le code attend un fichier mais a reçu un répertoire, souvent signe d’une validation incomplète.
Dans un service exposé au réseau, cette cartographie compte autant que le code lui-même, parce qu’elle permet de renvoyer la bonne réponse sans masquer le vrai problème. Et dans les environnements durcis, il faut encore ajouter une couche de restriction supplémentaire.
Depuis les versions récentes de Node.js, le mode de permissions peut limiter l’accès au système de fichiers quand l’application est lancée avec `--permission`. Si votre déploiement l’utilise, il faut tester le serveur avec exactement les chemins autorisés, sinon un accès parfaitement valide dans le code peut être refusé à l’exécution.
Les repères que j’applique avant de toucher au disque en production
- Je pars par défaut sur `node:fs/promises` pour garder un code clair et maintenable.
- Je réserve les fonctions synchrones aux scripts, aux tests et au démarrage du service.
- Je passe aux streams dès qu’un fichier devient gros, fréquent ou sensible en mémoire.
- Je valide toujours les chemins avant toute lecture, écriture ou suppression.
- Je traite les erreurs disque comme des cas normaux, avec un mapping HTTP cohérent.
- Je favorise l’écriture temporaire puis le renommage pour tout fichier important.
Si je devais résumer ma pratique en une seule idée, ce serait celle-ci : le bon usage du module de fichiers ne consiste pas à empiler des appels `fs`, mais à choisir le bon mode d’accès, au bon moment, avec des garde-fous simples. C’est ce qui permet à une API de rester rapide, prévisible et sûre quand elle commence vraiment à travailler avec le système de fichiers.