These five libraries address the challenge of parsing multipart/form-data requests in Node.js, a standard format for uploading files and submitting forms. busboy is a low-level, high-performance streaming parser that serves as the engine for many higher-level tools. multer is the most popular middleware for Express, built on top of busboy, offering easy disk and memory storage configuration. formidable is a robust, standalone parser known for its reliability and progress tracking, often used outside of Express. express-fileupload provides a simple, express-specific interface that mimics the old req.files object without requiring middleware setup. Finally, form-data is distinct from the others; it is primarily used to construct and send multipart requests (acting as a client) rather than parsing incoming ones, though it is often grouped in this ecosystem due to its name and related functionality.
Uploading files is a common requirement in web applications, but handling multipart/form-data in Node.js can be tricky. Unlike simple JSON payloads, file uploads involve streaming binary data, managing boundaries, and handling large payloads efficiently. The ecosystem offers several tools, each with a specific role. Let's break down how busboy, multer, formidable, express-fileupload, and form-data differ and when to use them.
The most important distinction is where these libraries sit in your stack. Some are raw engines, while others are complete solutions.
busboy is a streaming parser. It does not save files or manage storage. It simply parses the incoming stream and emits events for files and fields. You must write the logic to pipe these streams to disk or memory.
// busboy: Manual stream handling
import busboy from 'busboy';
function handler(req, res) {
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file, info) => {
// You must manually pipe to a destination
file.pipe(require('fs').createWriteStream(`./uploads/${info.filename}`));
});
req.pipe(bb);
}
multer is Express middleware built on top of busboy. It handles the parsing AND the storage logic automatically. You configure a "storage engine," and it takes care of the rest.
// multer: Configured middleware
import multer from 'multer';
const upload = multer({ dest: 'uploads/' });
app.post('/profile', upload.single('avatar'), (req, res) => {
// File is already saved; req.file contains metadata
res.send('Upload complete');
});
formidable is a standalone parser similar to busboy but includes built-in file writing capabilities. It works with any Node.js HTTP server, not just Express.
// formidable: Standalone with auto-saving
import { IncomingForm } from 'formidable';
const form = new IncomingForm({ uploadDir: './uploads' });
form.parse(req, (err, fields, files) => {
// Files are automatically written to uploadDir
console.log(files.avatar.filepath);
});
express-fileupload is a lightweight Express middleware that adds a files property to the request object. It simplifies the API but lacks the advanced storage configuration of multer.
// express-fileupload: Simple property injection
import fileUpload from 'express-fileupload';
app.use(fileUpload());
app.post('/upload', (req, res) => {
// Direct access to file data
req.files.avatar.mv('./uploads/avatar.png');
});
form-data is different. It is used to create multipart requests, not parse them. You use this when your Node.js server needs to act as a client and upload a file to another service.
// form-data: Constructing an outgoing request
import FormData from 'form-data';
import fetch from 'node-fetch';
const form = new FormData();
form.append('avatar', require('fs').createReadStream('./pic.png'));
fetch('https://api.example.com/upload', {
method: 'POST',
body: form
});
How you save files is often the deciding factor. multer shines here with its pluggable storage engines.
multer allows you to swap between memory storage and disk storage easily, and even define custom filenames.
// multer: Disk storage with custom filename
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 uses a simpler approach. Files are temporarily stored, and you must explicitly call .mv() to move them permanently. If you forget, temp files might linger.
// express-fileupload: Explicit move required
if (req.files && req.files.photo) {
req.files.photo.mv('/permanent/path/photo.png');
}
formidable automatically saves files to a configured directory but gives you the final path in the callback. It handles cleanup of temporary names internally.
// formidable: Auto-save to configured dir
const form = new IncomingForm({
uploadDir: './data',
keepExtensions: true
});
// File saved automatically, path returned in 'files' object
busboy gives you zero storage logic. You get a stream, and you decide where it goes. This is powerful for uploading directly to cloud storage (like S3) without touching the local disk.
// busboy: Streaming directly to S3 (conceptual)
bb.on('file', (name, file) => {
s3UploadStream.write(file);
});
Production apps need to prevent abuse. You must limit file sizes and counts.
multer has robust built-in limits for file size, number of files, and field count.
// multer: Enforcing limits
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
files: 1
}
});
formidable also supports limits, configured via the constructor options.
// formidable: Setting max file size
const form = new IncomingForm({
maxFileSize: 5 * 1024 * 1024,
maxFiles: 1
});
busboy requires you to handle limits manually by listening to the limit event or checking stream sizes, offering more flexibility but more code.
// busboy: Manual limit handling
bb.on('limit', () => {
// Handle file size or count limit exceeded
res.status(413).send('Too large');
});
For large files, users want to see progress bars. Not all libraries support this easily.
formidable excels here. It emits progress events naturally, making it easy to track upload percentage.
// formidable: Progress events
form.on('progress', (bytesReceived, bytesExpected) => {
const percent = (bytesReceived / bytesExpected) * 100;
console.log(`Upload: ${percent}%`);
});
multer and express-fileupload do not emit progress events by default because they rely on the underlying stream handling of Express/Busboy. Implementing progress requires dropping down to lower-level stream events or using additional middleware.
busboy allows you to calculate progress manually by tracking byte counts on the file stream, but it requires custom implementation.
You are building a REST API with Express and need to save user avatars to disk.
multerYou are using a lightweight framework like Fastify or the native http module and need to parse uploads.
formidable or busboyformidable offers a good balance of features without Express dependencies. busboy is better if you need to stream directly to another service (like AWS S3) without saving to the local filesystem first.You need to test a file upload endpoint in 5 minutes.
express-fileuploadapp.use(fileUpload()) and access req.files. No storage config needed for basic tests.Your server needs to forward an uploaded file to an external image processing service.
form-databusboy/multer into a form-data instance to forward it.It is critical to note the maintenance status of these libraries:
formidable: Historically very stable, but version 1.x had known issues. Version 2.x and 3.x are modernized and actively maintained. Do not use v1.x; ensure you install the latest version.multer: Widely used and stable, but development has slowed. It is still the standard for Express, but be aware that major updates are rare. It is safe for production but verify compatibility with newer Node versions if using edge features.busboy: Actively maintained and the recommended low-level engine by the Node.js community.express-fileupload: Maintained, but simpler. Suitable for less critical paths.form-data: Essential tool, actively maintained, and standard for client-side multipart construction.| Feature | multer | formidable | busboy | express-fileupload | form-data |
|---|---|---|---|---|---|
| Primary Role | Express Middleware | Standalone Parser | Streaming Parser | Express Middleware | Request Constructor |
| Framework | Express Only | Any (Vanilla, Fastify, etc.) | Any | Express Only | Any (Client-side) |
| Storage | Configurable (Disk/Memory) | Auto-save to Dir | Manual (Stream) | Manual (.mv()) | N/A (Sends data) |
| Progress Events | No (Native) | Yes | Manual | No | N/A |
| Ease of Use | High | Medium | Low (Verbose) | Very High | High |
| Best For | Standard Express Apps | Custom Servers / Progress | High Perf / Cloud Streams | Prototypes | Sending Files |
Choosing the right tool depends on your server architecture and how much control you need.
multer is the pragmatic choice for most Express developers. It removes boilerplate and handles the messy parts of file storage securely. If you are in the Express ecosystem, start here.
formidable is the robust alternative for non-Express environments or when you need upload progress tracking. It strikes a great balance between power and ease of use.
busboy is the specialist's tool. Use it when you need to stream files directly to cloud storage, process video/audio in real-time, or build your own high-performance upload server.
express-fileupload is the quick fix. Great for hacks, prototypes, or internal tools where advanced storage logic isn't needed.
form-data is the companion tool. While the others help you receive files, form-data helps you send them. You will likely use this alongside one of the parsers when your app needs to proxy uploads to other services.
Final Thought: Avoid reinventing the wheel. For standard web apps, multer or formidable will cover 95% of your needs safely and efficiently. Only drop down to busboy if you have specific streaming requirements that higher-level abstractions cannot meet.
Choose busboy if you need maximum performance and fine-grained control over the streaming process without the overhead of a full framework. It is ideal for building custom upload handlers, processing large files in real-time, or integrating into non-Express servers like Fastify or Koa where you need to manually pipe streams.
Choose express-fileupload if you want the simplest possible setup for an Express app and prefer accessing files directly via req.files without configuring storage engines. It is best for small projects, prototypes, or scenarios where files are small enough to be held in memory without complex disk management logic.
Choose form-data when your application needs to send multipart requests to other APIs or servers, rather than receive them. It is the standard tool for constructing HTTP POST requests with files and fields in Node.js scripts, microservices, or testing suites.
Choose formidable if you need a battle-tested, framework-agnostic parser that supports upload progress events and works well with vanilla Node.js http servers. It is a strong choice for applications requiring detailed feedback on upload status or those avoiding Express-specific middleware.
Choose multer if you are building an Express application and need a reliable, feature-rich solution for saving files to disk or memory with minimal code. It is the industry standard for Express file uploads, offering built-in support for file filtering, limits, and multiple storage strategies.
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.