Photo de profil, pièce jointe, import de CSV… L’upload de fichiers avec Node.js est une fonctionnalité qu’on retrouve dans presque toutes les applications. Dans ce guide, tu construis pas à pas une API d’envoi de fichiers avec Express et Multer : stockage sur disque, limites de taille, filtrage par type, envois multiples, barre de progression et, surtout, toutes les protections de sécurité indispensables.
L’upload de fichiers Node.js paraît simple : un formulaire, un bouton « Envoyer », et le fichier arrive sur le serveur. En réalité, c’est l’une des fonctionnalités les plus sensibles d’une application web. Un fichier trop gros peut saturer ton disque, un faux fichier image peut contenir un script, et un nom de fichier bien choisi peut écraser un fichier existant.

Dans ce tutoriel, on utilise Multer, le middleware de référence pour gérer les formulaires multipart/form-data avec Express. Tu vas partir d’un dossier vide et arriver à une API complète et sécurisée, que tu pourras réutiliser dans tes projets.

- Une API Express qui reçoit un ou plusieurs fichiers.
- Un stockage sur disque avec des noms de fichiers uniques et sûrs.
- Des limites de taille et de nombre, un filtre par type de fichier.
- Une vérification du vrai contenu du fichier (et pas seulement de son extension).
- Une page HTML avec glisser-déposer et barre de progression.
Prérequis
| Outil | Version | Pourquoi |
|---|---|---|
| Node.js | 20 LTS ou plus | Exécuter le serveur |
| npm | Fourni avec Node.js | Installer Express et Multer |
| Un éditeur | VS Code, WebStorm… | Écrire le code |
| Postman ou curl | Facultatif | Tester l’API sans interface |
Si tu n’as jamais créé de serveur Express, pas d’inquiétude : chaque fichier est expliqué ligne par ligne.
Comment fonctionne l’upload de fichiers avec Node.js ?
Quand un formulaire HTML envoie un fichier, le navigateur n’utilise pas l’encodage habituel (application/x-www-form-urlencoded) mais le format multipart/form-data. La requête est découpée en plusieurs « parties » : une pour chaque champ texte et une pour chaque fichier, séparées par une frontière (boundary).
Express ne sait pas lire ce format nativement : express.json() et express.urlencoded() l’ignorent. C’est le rôle de Multer :
- il lit le flux de la requête au fur et à mesure (sans tout charger en mémoire) ;
- il enregistre chaque fichier selon le stockage que tu as choisi (disque ou mémoire) ;
- il ajoute les informations du fichier dans
req.file(oureq.files) et les champs texte dansreq.body.
Multer est maintenu par l’équipe d’Express, il s’intègre comme un middleware classique et s’appuie sur busboy, un analyseur multipart rapide qui traite les données en flux. Des alternatives existent (formidable, busboy directement), mais Multer reste le choix le plus simple et le plus documenté pour une application Express.
Étape 1 : créer le projet
Ouvre un terminal et crée le dossier du projet :
mkdir upload-api && cd upload-api
npm init -y
npm install express multer@^2.2.0 file-type
mkdir uploads publicBashDans package.json, ajoute "type": "module" pour utiliser la syntaxe import, ainsi qu’un script de démarrage :
{
"name": "upload-api",
"type": "module",
"scripts": {
"dev": "node --watch server.js",
"start": "node server.js"
}
}JSONLe 30 juin 2026, l’équipe Express a publié Multer 2.2.0, qui corrige deux failles de déni de service : CVE-2026-5079 (champs imbriqués à l’infini qui épuisent la mémoire) et CVE-2026-5038 (fichiers partiels laissés sur le disque quand l’envoi est interrompu). Si ton projet utilise encore Multer 1.x, npm update ne suffit pas : installe explicitement npm install multer@^2.2.0.
L’arborescence finale du projet :
upload-api/
├── package.json
├── server.js ← le serveur Express
├── upload.js ← la configuration de Multer
├── uploads/ ← les fichiers reçus (hors du dossier public !)
└── public/
└── index.html ← la page d'envoi avec glisser-déposerTexteÉtape 2 : un premier upload de fichier Node.js
Commençons par la version la plus simple possible, pour comprendre le mécanisme :
// server.js (version 1, pour comprendre)
import express from 'express';
import multer from 'multer';
const app = express();
const upload = multer({ dest: 'uploads/' });
// 'fichier' doit correspondre au name="fichier" du champ du formulaire
app.post('/api/upload', upload.single('fichier'), (req, res) => {
console.log(req.file); // informations sur le fichier
console.log(req.body); // champs texte du formulaire
res.json({ message: 'Fichier reçu', fichier: req.file.filename });
});
app.listen(3000, () => console.log('API sur http://localhost:3000'));JavaScriptLance npm run dev puis teste avec curl :
curl -F "[email protected]" -F "titre=Vacances" http://localhost:3000/api/uploadBashDans la console, req.file ressemble à ceci :
{
fieldname: 'fichier',
originalname: 'photo.jpg',
encoding: '7bit',
mimetype: 'image/jpeg',
destination: 'uploads/',
filename: '3f2a9c1e8b7d6a5f4e3d2c1b0a998877',
path: 'uploads/3f2a9c1e8b7d6a5f4e3d2c1b0a998877',
size: 248137
}JavaScriptÇa fonctionne, mais cette version a trois défauts sérieux : aucune limite de taille, aucun filtre de type et un nom de fichier sans extension. On corrige tout ça.
Étape 3 : configurer le stockage sur disque
Avec diskStorage, tu décides toi-même du dossier et du nom de chaque fichier. Crée un fichier upload.js dédié à la configuration :
// upload.js
import multer from 'multer';
import path from 'node:path';
import crypto from 'node:crypto';
const EXTENSIONS = {
'image/jpeg': '.jpg',
'image/png': '.png',
'image/webp': '.webp',
'application/pdf': '.pdf',
};
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, path.resolve('uploads'));
},
filename: (req, file, cb) => {
// Nom aléatoire + extension déduite du type : on n'utilise JAMAIS le nom d'origine
const nom = crypto.randomUUID() + EXTENSIONS[file.mimetype];
cb(null, nom);
},
});JavaScriptPourquoi ne pas garder le nom d’origine ? Parce qu’il vient de l’utilisateur. Un nom comme ../../server.js pourrait tenter d’écrire hors du dossier prévu (path traversal), deux utilisateurs peuvent envoyer photo.jpg en même temps, et des caractères spéciaux peuvent poser problème selon le système. Un UUID est unique, prévisible pour toi et sans danger. Si tu veux afficher le nom d’origine, garde-le en base de données, pas sur le disque.
Étape 4 : limiter la taille et filtrer les types de fichiers
On ajoute maintenant deux protections essentielles : les limites et le filtre.
// upload.js (suite)
const TYPES_AUTORISES = Object.keys(EXTENSIONS);
function fileFilter(req, file, cb) {
if (TYPES_AUTORISES.includes(file.mimetype)) {
cb(null, true);
} else {
const erreur = new multer.MulterError('LIMIT_UNEXPECTED_FILE', file.fieldname);
erreur.message = 'Type de fichier non autorisé : ' + file.mimetype;
cb(erreur);
}
}
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5 Mo maximum par fichier
files: 5, // 5 fichiers maximum par requête
fields: 10, // 10 champs texte maximum
fieldSize: 100 * 1024, // 100 Ko maximum par champ texte
parts: 15, // total fichiers + champs
},
});JavaScript| Limite | Rôle | Valeur conseillée |
|---|---|---|
fileSize | Taille maximale d’un fichier | Selon l’usage : 2 à 10 Mo pour des images |
files | Nombre de fichiers par requête | Le strict nécessaire |
fields | Nombre de champs texte | Le nombre de champs de ton formulaire |
fieldSize | Taille d’un champ texte | 100 Ko suffisent largement |
parts | Total des parties | files + fields |
file.mimetype est déclaré par le client. Un attaquant peut envoyer un script PHP en prétendant que c’est une image/jpeg. Le filtre de Multer est une première barrière pratique, mais la vraie vérification se fait sur le contenu du fichier, à l’étape 6.
Étape 5 : un, plusieurs ou des champs différents
Multer propose plusieurs méthodes selon la forme de ton formulaire :
| Méthode | Cas d’usage | Résultat |
|---|---|---|
upload.single('avatar') | Un seul fichier | req.file |
upload.array('photos', 5) | Plusieurs fichiers dans le même champ | req.files (tableau) |
upload.fields([{ name: 'cv', maxCount: 1 }, { name: 'lettres', maxCount: 3 }]) | Plusieurs champs fichiers | req.files.cv, req.files.lettres |
upload.none() | Formulaire multipart sans fichier | req.body uniquement |
upload.any() | N’importe quel champ | À éviter : tu ne contrôles plus rien |
Exemple avec une galerie de photos :
app.post('/api/galerie', upload.array('photos', 5), (req, res) => {
const fichiers = req.files.map(f => ({ nom: f.filename, taille: f.size }));
res.status(201).json({ total: fichiers.length, fichiers });
});JavaScriptAvec any(), n’importe quel champ fichier est accepté, y compris ceux que tu n’attendais pas. Déclare toujours précisément les champs avec single, array ou fields : c’est plus sûr et le code est plus lisible.
Étape 6 : vérifier le vrai contenu du fichier
Chaque format de fichier commence par une signature binaire, appelée nombre magique : un JPEG commence par les octets FF D8 FF, un PNG par 89 50 4E 47, un PDF par %PDF. Le paquet file-type lit ces premiers octets et te dit ce que le fichier est vraiment.
// verifier.js
import { fileTypeFromFile } from 'file-type';
import fs from 'node:fs/promises';
const AUTORISES = new Set(['image/jpeg', 'image/png', 'image/webp', 'application/pdf']);
export async function verifierContenu(req, res, next) {
const fichiers = req.file ? [req.file] : (req.files ?? []);
try {
for (const f of fichiers) {
const type = await fileTypeFromFile(f.path);
if (!type || !AUTORISES.has(type.mime) || type.mime !== f.mimetype) {
// On supprime TOUS les fichiers de la requête, pas seulement le fautif
await Promise.all(fichiers.map(x => fs.rm(x.path, { force: true })));
return res.status(415).json({ erreur: `Contenu invalide pour « ${f.originalname} »` });
}
}
next();
} catch (erreur) {
next(erreur);
}
}JavaScriptEt on l’enchaîne juste après Multer :
app.post('/api/upload', upload.single('fichier'), verifierContenu, (req, res) => {
res.status(201).json({ fichier: req.file.filename, taille: req.file.size });
});JavaScriptAinsi, un fichier .exe renommé en photo.jpg est détecté et supprimé immédiatement.
Étape 7 : gérer proprement les erreurs
Quand une limite est dépassée, Multer lève une MulterError avec un code précis. Sans gestionnaire d’erreurs, Express renverrait une page HTML générique avec un statut 500. On ajoute un middleware d’erreur qui traduit chaque cas en réponse JSON claire :
// server.js (en bas, après toutes les routes)
import multer from 'multer';
const MESSAGES = {
LIMIT_FILE_SIZE: 'Fichier trop volumineux (5 Mo maximum).',
LIMIT_FILE_COUNT: 'Trop de fichiers (5 maximum).',
LIMIT_UNEXPECTED_FILE: 'Champ de fichier inattendu ou type non autorisé.',
LIMIT_FIELD_COUNT: 'Trop de champs dans le formulaire.',
LIMIT_PART_COUNT: 'Formulaire trop complexe.',
};
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
const statut = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
return res.status(statut).json({ erreur: MESSAGES[err.code] ?? err.message });
}
console.error(err);
res.status(500).json({ erreur: 'Erreur interne du serveur.' });
});JavaScript| Code Multer | Statut HTTP | Signification |
|---|---|---|
LIMIT_FILE_SIZE | 413 | Le fichier dépasse fileSize |
LIMIT_FILE_COUNT | 400 | Plus de fichiers que files |
LIMIT_UNEXPECTED_FILE | 400 | Champ inconnu ou refusé par le filtre |
LIMIT_FIELD_COUNT | 400 | Trop de champs texte |
LIMIT_PART_COUNT | 400 | Trop de parties au total |
Étape 8 : le serveur complet
Voici server.js dans sa version finale, qui assemble tout :
// server.js
import express from 'express';
import multer from 'multer';
import path from 'node:path';
import { upload } from './upload.js';
import { verifierContenu } from './verifier.js';
const app = express();
app.use(express.static('public')); // la page d'envoi
app.post('/api/upload', upload.single('fichier'), verifierContenu, (req, res) => {
if (!req.file) {
return res.status(400).json({ erreur: 'Aucun fichier reçu.' });
}
res.status(201).json({
id: req.file.filename,
nomOrigine: req.file.originalname,
taille: req.file.size,
type: req.file.mimetype,
});
});
app.post('/api/galerie', upload.array('photos', 5), verifierContenu, (req, res) => {
res.status(201).json({ total: req.files.length, ids: req.files.map(f => f.filename) });
});
// Téléchargement contrôlé : on n'expose jamais le dossier uploads directement
app.get('/api/fichiers/:id', (req, res) => {
const id = path.basename(req.params.id); // neutralise les « ../ »
res.sendFile(path.resolve('uploads', id), {
headers: { 'X-Content-Type-Options': 'nosniff' },
}, err => {
if (err) res.status(404).json({ erreur: 'Fichier introuvable.' });
});
});
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
const statut = err.code === 'LIMIT_FILE_SIZE' ? 413 : 400;
return res.status(statut).json({ erreur: err.message });
}
console.error(err);
res.status(500).json({ erreur: 'Erreur interne du serveur.' });
});
app.listen(3000, () => console.log('API d’upload sur http://localhost:3000'));JavaScriptServir le dossier directement permettrait d’accéder à n’importe quel fichier sans contrôle. Avec une route dédiée, tu peux ajouter une vérification de droits (l’utilisateur a-t-il le droit de voir ce fichier ?), journaliser les téléchargements et forcer des en-têtes de sécurité.
Étape 9 : la page d’envoi avec glisser-déposer et progression
fetch() ne permet pas encore de suivre la progression d’un envoi dans tous les navigateurs. On utilise donc XMLHttpRequest, qui expose l’événement upload.onprogress :
<!-- public/index.html -->
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Envoi de fichiers</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 560px; margin: 40px auto; }
#zone { border: 2px dashed #94a3b8; border-radius: 14px; padding: 40px; text-align: center; cursor: pointer; }
#zone.survol { border-color: #2563eb; background: #eff6ff; }
progress { width: 100%; height: 14px; margin-top: 16px; }
</style>
</head>
<body>
<h1>Envoyer un fichier</h1>
<div id="zone">Glisse ton fichier ici ou clique pour le choisir
<small>JPG, PNG, WebP ou PDF · 5 Mo max</small></div>
<input type="file" id="choix" accept=".jpg,.jpeg,.png,.webp,.pdf" hidden>
<progress id="barre" value="0" max="100" hidden></progress>
<p id="statut"></p>
<script>
const zone = document.getElementById('zone');
const choix = document.getElementById('choix');
const barre = document.getElementById('barre');
const statut = document.getElementById('statut');
zone.addEventListener('click', () => choix.click());
choix.addEventListener('change', () => envoyer(choix.files[0]));
zone.addEventListener('dragover', e => { e.preventDefault(); zone.classList.add('survol'); });
zone.addEventListener('dragleave', () => zone.classList.remove('survol'));
zone.addEventListener('drop', e => {
e.preventDefault();
zone.classList.remove('survol');
envoyer(e.dataTransfer.files[0]);
});
function envoyer(fichier) {
if (!fichier) return;
if (fichier.size > 5 * 1024 * 1024) {
statut.textContent = 'Fichier trop volumineux (5 Mo maximum).';
return;
}
const donnees = new FormData();
donnees.append('fichier', fichier);
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/upload');
barre.hidden = false;
xhr.upload.onprogress = e => {
if (e.lengthComputable) barre.value = Math.round((e.loaded / e.total) * 100);
};
xhr.onload = () => {
const reponse = JSON.parse(xhr.responseText);
statut.textContent = xhr.status === 201
? `✅ « ${reponse.nomOrigine} » envoyé (${Math.round(reponse.taille / 1024)} Ko)`
: `❌ ${reponse.erreur}`;
};
xhr.onerror = () => { statut.textContent = '❌ Erreur réseau.'; };
xhr.send(donnees);
}
</script>
</body>
</html>HTMLRemarque : on vérifie la taille aussi côté navigateur, pour éviter à l’utilisateur d’attendre la fin d’un envoi qui sera refusé. Mais cette vérification est uniquement un confort : la vraie limite reste celle du serveur, que personne ne peut contourner.
Stockage en mémoire, cloud et redimensionnement d’images
Le stockage sur disque convient à la plupart des projets. Deux autres cas reviennent souvent.
Traiter le fichier avant de l’enregistrer (redimensionner une photo, par exemple) : utilise memoryStorage. Le fichier est disponible dans req.file.buffer, sans passer par le disque.
import multer from 'multer';
import sharp from 'sharp'; // npm install sharp
import crypto from 'node:crypto';
import path from 'node:path';
const uploadMemoire = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024, files: 1 },
});
app.post('/api/avatar', uploadMemoire.single('avatar'), async (req, res, next) => {
try {
const nom = crypto.randomUUID() + '.webp';
await sharp(req.file.buffer)
.rotate() // respecte l'orientation EXIF
.resize(256, 256, { fit: 'cover' })
.webp({ quality: 85 })
.toFile(path.resolve('uploads', nom)); // les métadonnées EXIF (GPS…) ne sont pas recopiées
res.status(201).json({ avatar: nom });
} catch (erreur) {
next(erreur);
}
});JavaScriptEn mémoire, chaque fichier occupe de la RAM jusqu’à la fin du traitement. Dix envois simultanés de 50 Mo, c’est 500 Mo de mémoire. Garde memoryStorage pour de petits fichiers, avec une limite fileSize basse.
Stocker dans le cloud (Amazon S3, Cloudflare R2, Google Cloud Storage) : pour une application déployée sur plusieurs serveurs ou sur une plateforme sans disque persistant, envoie le fichier vers un service de stockage objet. Des moteurs de stockage Multer existent (comme multer-s3), ou tu peux envoyer le buffer avec le SDK du fournisseur.
La checklist sécurité de l’upload de fichiers Node.js
- Multer en version 2.2.0 ou plus, dépendances auditées avec
npm audit. - Limites
fileSize,files,fieldsetpartstoujours définies. - Liste blanche des types autorisés, jamais de liste noire.
- Vérification du contenu réel avec
file-type. - Nom de fichier généré par le serveur (UUID), jamais le nom d’origine.
- Dossier
uploads/en dehors du dossier public, servi par une route contrôlée. - En-tête
X-Content-Type-Options: nosniffsur les téléchargements. - Contrôle d’accès : seul le propriétaire (ou un rôle autorisé) peut télécharger un fichier.
- Limite de taille aussi au niveau du serveur frontal (Nginx
client_max_body_size). - Pour les plateformes publiques, analyse antivirus (ClamAV par exemple) des fichiers reçus.
Ces règles correspondent directement aux catégories de l’OWASP Top 10, en particulier la mauvaise configuration et la chaîne d’approvisionnement.
Dépannage
| Problème | Cause | Solution |
|---|---|---|
req.file est undefined | Le nom du champ ne correspond pas | upload.single('fichier') doit correspondre à name="fichier" (ou FormData.append('fichier', …)) |
req.body est vide | Les champs texte sont envoyés après le fichier | Place les champs texte avant le fichier dans le formulaire |
Erreur Unexpected field | Champ fichier non déclaré | Déclare-le dans single, array ou fields |
Erreur ENOENT à l’enregistrement | Le dossier de destination n’existe pas | Crée uploads/ (ou fs.mkdirSync('uploads', { recursive: true })) |
| Le fichier arrive à 0 octet | Tu as défini Content-Type à la main dans fetch | Laisse le navigateur le définir : il doit inclure la frontière (boundary) |
| Erreur CORS depuis un autre domaine | Front et API sur des origines différentes | Configure CORS côté serveur, voir notre guide « CORS policy blocked » |

FAQ
Multer fonctionne-t-il avec Express 5 ?
Oui. Multer est maintenu par l’équipe d’Express et fonctionne avec Express 4 et Express 5. Avec Express 5, les erreurs levées dans un gestionnaire asynchrone sont transmises automatiquement au middleware d’erreur.
Quelle taille maximale autoriser pour un upload de fichiers Node.js ?
Il n’y a pas de valeur universelle : choisis la plus petite qui couvre ton usage réel. 2 à 5 Mo pour des photos de profil, 10 à 20 Mo pour des documents. Pour des vidéos, préfère un envoi direct vers un stockage cloud avec une URL présignée.
Faut-il stocker les fichiers en base de données ?
Rarement. Stocke le fichier sur disque ou dans un stockage objet, et garde en base uniquement ses métadonnées : identifiant, nom d’origine, taille, type, propriétaire, date. La base reste légère et les sauvegardes rapides.
Comment supprimer les fichiers orphelins ?
Enregistre chaque fichier en base au moment de l’envoi, puis planifie une tâche (par exemple chaque nuit) qui supprime les fichiers du disque absents de la base, et inversement.
Conclusion
Tu disposes maintenant d’une API d’upload de fichiers Node.js complète et sécurisée : Multer pour lire les formulaires multipart, un stockage avec des noms générés, des limites strictes, une vérification du contenu réel, une gestion d’erreurs claire et une interface avec glisser-déposer et progression.
La prochaine étape logique : associer chaque fichier à un utilisateur en base de données et vérifier ses droits sur la route de téléchargement. Et si ton application doit prévenir l’utilisateur par e-mail quand son fichier a été traité, notre guide pour envoyer des e-mails avec Node.js et Nodemailer te montre comment faire. La documentation complète de Multer est disponible sur son dépôt GitHub officiel.

