These libraries solve the problem of parsing multipart/form-data requests, which is the standard way browsers send files and form fields to a server. busboy is a low-level streaming parser that powers many other tools. formidable is a mature, standalone parser that handles both files and form fields with a robust API. multer is the most popular middleware for Express, built on top of busboy, designed specifically for saving files to disk or memory. express-fileupload offers a simpler, promise-friendly interface for Express without requiring middleware setup for every route. express-formidable is a thin wrapper that brings formidable into the Express middleware chain. connect-multiparty is an older middleware based on an outdated version of multiparty and is no longer maintained.
Uploading files is a common requirement in web applications, but handling multipart/form-data in Node.js is not as simple as reading JSON. The data arrives in chunks, mixed with boundaries and headers, requiring a parser to separate files from form fields. The ecosystem offers several tools, ranging from low-level streaming engines to high-level Express middleware. Let's break down how they differ in architecture, ease of use, and real-world application.
The first distinction to make is whether you need a raw parsing engine or a ready-to-use middleware.
busboy is a streaming parser. It does not save files or manage storage; it simply emits events as it finds data. You must write the code to handle those streams.
// busboy: Manual stream handling
import Busboy from 'busboy';
function handler(req, res) {
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, info) => {
const { filename, encoding, mimeType } = info;
// You must manually pipe this to a destination
file.pipe(require('fs').createWriteStream(`./uploads/${filename}`));
});
req.pipe(busboy);
}
multer, express-fileupload, and express-formidable are middleware. They sit between the request and your route handler, doing the heavy lifting before your code runs.
// multer: Middleware approach
import multer from 'multer';
const upload = multer({ dest: 'uploads/' });
app.post('/profile', upload.single('avatar'), (req, res) => {
// File is already saved and info is in req.file
res.send('Upload complete');
});
// express-fileupload: Middleware approach
import fileUpload from 'express-fileupload';
app.use(fileUpload());
app.post('/profile', (req, res) => {
// File is available directly on req.files
if (!req.files || !req.files.avatar) return res.status(400).send('No file');
req.files.avatar.mv('./uploads/' + req.files.avatar.name);
res.send('Upload complete');
});
formidable is a standalone parser that can act like middleware if wrapped, but it is often used directly with native Node HTTP servers.
// formidable: Standalone parser
import { IncomingForm } from 'formidable';
const form = new IncomingForm();
form.parse(req, (err, fields, files) => {
// Files are parsed and saved to a temp directory by default
console.log(files.avatar.filepath);
});
connect-multiparty is officially deprecated. It hasn't been updated in years and depends on older versions of parsing libraries that may have security vulnerabilities.
// ❌ DO NOT USE THIS
// import multiparty from 'connect-multiparty';
// This package is abandoned and should be removed from any modern stack.
If you see this in a legacy project, replace it with multer for Express apps or formidable for generic Node servers. There is no technical reason to start a new project with this tool.
How much control do you need over where the file goes and what it's called?
multer gives you explicit control via "Storage Engines." You can define exactly where files go, how they are named, and even limit file types.
// multer: Custom storage engine
const storage = multer.diskStorage({
destination: (req, file, cb) => cb(null, 'uploads/'),
filename: (req, file, cb) => cb(null, Date.now() + '-' + file.originalname)
});
const upload = multer({ storage });
express-fileupload saves files to a temporary system folder by default. You must manually move them to their final destination using the .mv() method. This is simple but adds an extra step in your code.
// express-fileupload: Manual move required
if (req.files.sampleFile) {
const sampleFile = req.files.sampleFile;
sampleFile.mv('/var/www/uploads/filename.jpg', (err) => {
if (err) return res.status(500).send(err);
res.send('File moved successfully');
});
}
formidable also saves to a temporary directory automatically. It provides the filepath in the callback, expecting you to rename or move the file if needed.
// formidable: Temp file handling
form.on('file', (field, file) => {
// file.filepath points to the temp location
// You must use fs.rename to move it permanently
});
busboy offers the most control because you handle the stream directly. You decide instantly whether to pipe to disk, upload to S3, or discard the data based on content.
// busboy: Direct stream piping
busboy.on('file', (name, file) => {
// Pipe directly to an S3 upload stream without saving to disk first
file.pipe(s3UploadStream);
});
Preventing users from uploading dangerous files (like executables) is critical.
multer has a built-in fileFilter function that runs before the file is fully processed. You can reject files based on MIME type or extension immediately.
// multer: Built-in filtering
const upload = multer({
fileFilter: (req, file, cb) => {
if (file.mimetype !== 'image/png') {
return cb(new Error('Only PNG images are allowed'), false);
}
cb(null, true);
}
});
express-fileupload and formidable do not have built-in filters. You must check the file type manually in your route logic or middleware after the upload has already occurred. This means the file is already on your server (even if in a temp folder) before you decide to delete it.
// express-fileupload: Manual validation
if (req.files.doc.mimetype !== 'application/pdf') {
// File is already uploaded to temp; now you must delete it
return res.status(400).send('Invalid type');
}
busboy allows you to inspect the MIME type in the event handler and simply choose not to pipe the stream if it's invalid, effectively stopping the write process early.
// busboy: Early stream rejection
busboy.on('file', (name, file, info) => {
if (info.mimeType !== 'image/jpeg') {
// Just don't pipe it; let the stream end
return;
}
file.pipe(writeStream);
});
Most uploads include both a file and some text data (like a caption or user ID).
formidable excels here. It returns two distinct objects: fields for text data and files for binary data. This separation is clean and predictable.
// formidable: Separated data
form.parse(req, (err, fields, files) => {
const username = fields.username[0]; // Arrays by default
const avatar = files.avatar;
});
multer puts text fields on req.body and file info on req.file (or req.files). This feels very natural to Express developers since req.body is already used for JSON/urlencoded data.
// multer: Express-native separation
app.post('/post', upload.single('image'), (req, res) => {
const caption = req.body.caption;
const imageInfo = req.file;
});
express-fileupload puts everything on the req.files object if you configure it that way, or splits them depending on settings. By default, text fields often end up in req.body if the middleware is configured to parse them, but complex nested forms can sometimes be tricky compared to formidable.
// express-fileupload: Mixed access
app.post('/post', (req, res) => {
const caption = req.body.caption;
const image = req.files.image;
});
| Feature | multer | express-fileupload | formidable | busboy | connect-multiparty |
|---|---|---|---|---|---|
| Best For | Standard Express Apps | Simple Express Scripts | Standalone/Custom Servers | Custom Engines/Frameworks | ❌ Legacy Only |
| Setup | Middleware Config | Simple Middleware | Instance Creation | Event Listeners | Middleware |
| Storage | Configurable Engine | Temp + Manual Move | Temp + Manual Move | Manual Stream Pipe | Temp |
| Filtering | Built-in fileFilter | Manual Check | Manual Check | Manual Stream Logic | Basic |
| Maintenance | ✅ Active | ✅ Active | ✅ Active | ✅ Active | ❌ Deprecated |
For most Express developers, multer is the right choice. It is the industry standard, actively maintained, and offers the best balance of safety (filtering), configuration (storage engines), and ease of use. It integrates perfectly with the Express ecosystem.
If you are building a microservice or a script where you want the absolute simplest setup and don't need complex storage rules, express-fileupload is a solid, lightweight alternative.
If you are not using Express (e.g., using Fastify, Hapi, or raw Node HTTP), formidable is the most robust standalone parser available. It handles edge cases in multipart parsing better than almost anything else.
Only reach for busboy if you are building a library yourself or need to stream files directly to cloud storage (like S3) without touching the local disk.
And finally, if you see connect-multiparty, treat it as technical debt. Replace it immediately to ensure your application remains secure and maintainable.
Choose busboy if you are building a custom framework, need maximum performance via streaming, or require fine-grained control over how every byte of the upload is processed. It is not a ready-to-use solution for simple Express apps but rather the engine you build upon when existing middleware doesn't fit your specific architecture.
Do NOT choose connect-multiparty for any new project. It is deprecated, unmaintained, and relies on outdated dependencies with known security risks. If you encounter this in a legacy codebase, plan to migrate to multer or formidable immediately.
Choose express-fileupload if you want a lightweight, easy-to-setup solution for Express that accesses files directly from the request object (req.files) without configuring storage engines. It is ideal for small to medium services where you need quick implementation and don't require complex stream manipulation.
Choose express-formidable if you specifically need the robust feature set of formidable (like robust error handling and progress events) but want it to behave like standard Express middleware. It bridges the gap between the standalone formidable library and the Express request/response cycle.
Choose formidable if you are not using Express, need a standalone parser that works with raw Node.js HTTP servers, or require advanced features like upload progress tracking and detailed parsing events. It is a versatile, battle-tested library that works independently of any specific web framework.
Choose multer if you are building an Express application and need a reliable, standard way to save uploaded files to disk or memory. It is the community standard for Express file handling, offering configurable storage engines, file filtering, and seamless integration with the Express middleware pipeline.
A node.js module for parsing incoming HTML form data.
Changes (breaking or otherwise) in v1.0.0 can be found here.
npm install busboy
const http = require('http');
const busboy = require('busboy');
http.createServer((req, res) => {
if (req.method === 'POST') {
console.log('POST request');
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file, info) => {
const { filename, encoding, mimeType } = info;
console.log(
`File [${name}]: filename: %j, encoding: %j, mimeType: %j`,
filename,
encoding,
mimeType
);
file.on('data', (data) => {
console.log(`File [${name}] got ${data.length} bytes`);
}).on('close', () => {
console.log(`File [${name}] done`);
});
});
bb.on('field', (name, val, info) => {
console.log(`Field [${name}]: value: %j`, val);
});
bb.on('close', () => {
console.log('Done parsing form!');
res.writeHead(303, { Connection: 'close', Location: '/' });
res.end();
});
req.pipe(bb);
} else if (req.method === 'GET') {
res.writeHead(200, { Connection: 'close' });
res.end(`
<html>
<head></head>
<body>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="filefield"><br />
<input type="text" name="textfield"><br />
<input type="submit">
</form>
</body>
</html>
`);
}
}).listen(8000, () => {
console.log('Listening for requests');
});
// Example output:
//
// Listening for requests
// < ... form submitted ... >
// POST request
// File [filefield]: filename: "logo.jpg", encoding: "binary", mime: "image/jpeg"
// File [filefield] got 11912 bytes
// Field [textfield]: value: "testing! :-)"
// File [filefield] done
// Done parsing form!
const { randomFillSync } = require('crypto');
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const busboy = require('busboy');
const random = (() => {
const buf = Buffer.alloc(16);
return () => randomFillSync(buf).toString('hex');
})();
http.createServer((req, res) => {
if (req.method === 'POST') {
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file, info) => {
const saveTo = path.join(os.tmpdir(), `busboy-upload-${random()}`);
file.pipe(fs.createWriteStream(saveTo));
});
bb.on('close', () => {
res.writeHead(200, { 'Connection': 'close' });
res.end(`That's all folks!`);
});
req.pipe(bb);
return;
}
res.writeHead(404);
res.end();
}).listen(8000, () => {
console.log('Listening for requests');
});
busboy exports a single function:
( function )(< object >config) - Creates and returns a new Writable form parser stream.
Valid config properties:
headers - object - These are the HTTP headers of the incoming request, which are used by individual parsers.
highWaterMark - integer - highWaterMark to use for the parser stream. Default: node's stream.Writable default.
fileHwm - integer - highWaterMark to use for individual file streams. Default: node's stream.Readable default.
defCharset - string - Default character set to use when one isn't defined. Default: 'utf8'.
defParamCharset - string - For multipart forms, the default character set to use for values of part header parameters (e.g. filename) that are not extended parameters (that contain an explicit charset). Default: 'latin1'.
preservePath - boolean - If paths in filenames from file parts in a 'multipart/form-data' request shall be preserved. Default: false.
limits - object - Various limits on incoming data. Valid properties are:
fieldNameSize - integer - Max field name size (in bytes). Default: 100.
fieldSize - integer - Max field value size (in bytes). Default: 1048576 (1MB).
fields - integer - Max number of non-file fields. Default: Infinity.
fileSize - integer - For multipart forms, the max file size (in bytes). Default: Infinity.
files - integer - For multipart forms, the max number of file fields. Default: Infinity.
parts - integer - For multipart forms, the max number of parts (fields + files). Default: Infinity.
headerPairs - integer - For multipart forms, the max number of header key-value pairs to parse. Default: 2000 (same as node's http module).
This function can throw exceptions if there is something wrong with the values in config. For example, if the Content-Type in headers is missing entirely, is not a supported type, or is missing the boundary for 'multipart/form-data' requests.
file(< string >name, < Readable >stream, < object >info) - Emitted for each new file found. name contains the form field name. stream is a Readable stream containing the file's data. No transformations/conversions (e.g. base64 to raw binary) are done on the file's data. info contains the following properties:
filename - string - If supplied, this contains the file's filename. WARNING: You should almost never use this value as-is (especially if you are using preservePath: true in your config) as it could contain malicious input. You are better off generating your own (safe) filenames, or at the very least using a hash of the filename.
encoding - string - The file's 'Content-Transfer-Encoding' value.
mimeType - string - The file's 'Content-Type' value.
Note: If you listen for this event, you should always consume the stream whether you care about its contents or not (you can simply do stream.resume(); if you want to discard/skip the contents), otherwise the 'finish'/'close' event will never fire on the busboy parser stream.
However, if you aren't accepting files, you can either simply not listen for the 'file' event at all or set limits.files to 0, and any/all files will be automatically skipped (these skipped files will still count towards any configured limits.files and limits.parts limits though).
Note: If a configured limits.fileSize limit was reached for a file, stream will both have a boolean property truncated set to true (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
field(< string >name, < string >value, < object >info) - Emitted for each new non-file field found. name contains the form field name. value contains the string value of the field. info contains the following properties:
nameTruncated - boolean - Whether name was truncated or not (due to a configured limits.fieldNameSize limit)
valueTruncated - boolean - Whether value was truncated or not (due to a configured limits.fieldSize limit)
encoding - string - The field's 'Content-Transfer-Encoding' value.
mimeType - string - The field's 'Content-Type' value.
partsLimit() - Emitted when the configured limits.parts limit has been reached. No more 'file' or 'field' events will be emitted.
filesLimit() - Emitted when the configured limits.files limit has been reached. No more 'file' events will be emitted.
fieldsLimit() - Emitted when the configured limits.fields limit has been reached. No more 'field' events will be emitted.