busboy, connect-busboy, express-fileupload, formidable, and multer are all Node.js libraries designed to handle multipart/form-data requests, which are standard for file uploads and complex form submissions. busboy is a low-level, high-performance streaming parser that serves as the engine for many other libraries but requires manual wiring. connect-busboy is a specific middleware wrapper to use busboy within the Connect/Express ecosystem. multer is the most popular Express-specific middleware that simplifies busboy by adding configuration for storage destinations and file filtering. express-fileupload offers a simpler, promise-friendly interface that attaches files directly to the request object without complex setup. formidable is a robust, general-purpose parser that works with any Node.js HTTP server and has recently undergone a major v3 rewrite to support modern async/await patterns.
Handling file uploads is one of the most common yet tricky tasks in backend development. When a user submits a form with a file, the data arrives as multipart/form-data, a format that mixes binary file content with text fields. Node.js doesn't handle this out of the box, so we rely on specialized libraries.
The packages busboy, connect-busboy, express-fileupload, formidable, and multer all solve this problem, but they approach it from different angles. Some give you raw power and streams; others wrap that power in easy-to-use middleware. Let's break down how they work, their trade-offs, and when to use each one.
The biggest difference between these libraries is how they handle data flow. Do they stream data directly to your disk (saving memory), or do they load everything into RAM first?
busboy is a pure streaming parser. It does not save files for you. Instead, it emits events as data chunks arrive. You must listen to these events and decide where to pipe the data. This makes it incredibly memory-efficient but requires more code.
// busboy: Manual stream handling
import Busboy from 'busboy';
function handleUpload(req, res) {
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, info) => {
// You must manually pipe the stream to a destination
file.pipe(require('fs').createWriteStream(`./uploads/${info.filename}`));
});
req.pipe(busboy);
}
multer is built on top of busboy. It adds a middleware layer that automatically handles the streaming and saving logic based on your configuration. It streams data to disk or memory without loading the whole file into RAM first.
// multer: Configured middleware
import multer from 'multer';
const upload = multer({ dest: 'uploads/' });
app.post('/profile', upload.single('avatar'), (req, res) => {
// File is already saved to disk; req.file holds metadata
res.send('Upload complete');
});
express-fileupload takes a simpler approach. By default, it often buffers smaller files into memory before making them available on the request object. While convenient, this can be risky for very large files unless you configure it to use temporary files explicitly.
// express-fileupload: Simple attachment
import fileUpload from 'express-fileupload';
app.use(fileUpload());
app.post('/upload', (req, res) => {
// File is available as an object on req.files
if (!req.files || !req.files.photo) return res.status(400).send('No files');
req.files.photo.mv('./uploads/photo.png');
});
formidable (v3+) uses a modern promise-based API. It can operate in streaming mode or buffer mode depending on configuration. It is framework-agnostic, meaning it works with raw Node.js http modules as well as Express.
// formidable: Async/Await pattern
import { IncomingForm } from 'formidable';
app.post('/upload', async (req, res) => {
const form = new IncomingForm();
// Parse returns a promise with fields and files
const [fields, files] = await form.parse(req);
res.json({ filename: files.photo[0].originalFilename });
});
connect-busboy is simply a wrapper that makes the raw busboy library behave like standard Connect/Express middleware. It doesn't change the streaming nature of busboy; it just fits it into the app.use() chain.
// connect-busboy: Middleware wrapper for raw busboy
import busboy from 'connect-busboy';
app.use(busboy());
app.post('/upload', (req, res) => {
// You still need to manually access the busboy instance from req
const bb = req.busboy;
bb.on('file', (name, file) => {
file.pipe(require('fs').createWriteStream('./upload.txt'));
});
req.pipe(bb);
});
How much control do you have over what gets uploaded? Can you filter file types or limit sizes easily?
multer shines here. It offers a rich configuration object for storage engines (disk or memory), file filtering functions, and limits on file size and count. This makes it very safe for production use.
// multer: Advanced filtering and limits
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: storage,
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB limit
fileFilter: (req, file, cb) => {
if (file.mimetype === 'image/png') cb(null, true);
else cb(new Error('Only PNG allowed'));
}
});
express-fileupload keeps config minimal. You can set limits and temp file paths in the main setup, but it lacks the sophisticated "storage engine" abstraction found in Multer. It's great for simple cases but less flexible for complex renaming or storage logic.
// express-fileupload: Basic limits
app.use(fileUpload({
limits: { fileSize: 50 * 1024 * 1024 },
abortOnLimit: true,
useTempFiles: true,
tempFileDir: '/tmp/'
}));
formidable handles limits and renaming via its options object. Since v3, it is more modular, allowing you to plug in different parsers, but the API is slightly more verbose than Multer's shorthand.
// formidable: Option-based control
const form = new IncomingForm({
uploadDir: '/tmp/',
keepExtensions: true,
maxFileSize: 5 * 1024 * 1024,
filter: ({ name, originalFilename, mimetype }) => {
return mimetype === 'image/jpeg';
}
});
busboy and connect-busboy give you zero hand-holding. You must manually implement every check, limit, and error handler inside your event listeners. This is powerful but error-prone if you miss a check.
// busboy: Manual limit enforcement
const busboy = new Busboy({
headers: req.headers,
limits: { fileSize: 5 * 1024 * 1024 }
});
busboy.on('file', (name, file, info) => {
file.on('limit', () => {
// Must manually handle what happens when limit is hit
console.log('File too large');
file.destroy();
});
// Manual mime type check required
if (info.mimetype !== 'image/png') return file.destroy();
});
Not all libraries play nice with every framework. Some are locked to Express, while others work anywhere.
multer: Strictly for Express. It relies on Express middleware signatures and request properties.express-fileupload: Strictly for Express. The name says it all.connect-busboy: Designed for Connect and Express (since Express is built on Connect).busboy: Framework agnostic. Works with raw Node.js http, Fastify, Koa, or anything that exposes a stream.formidable: Framework agnostic. It accepts any Node.js IncomingMessage request object, making it versatile for Koa, Hapi, or raw servers.// Example: Using formidable in a raw Node.js server (no Express)
import http from 'http';
import { IncomingForm } from 'formidable';
const server = http.createServer((req, res) => {
if (req.url === '/upload' && req.method === 'POST') {
const form = new IncomingForm();
form.parse(req, (err, fields, files) => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify(files));
});
}
});
It is critical to note the status of connect-busboy. While it still functions, the ecosystem has largely moved towards multer for Express users or direct busboy usage for custom needs. connect-busboy is often considered legacy because it solves a problem (integrating busboy with Connect) that multer now solves more comprehensively. Do not start new projects with connect-busboy; choose multer for Express or busboy for custom streams.
formidable had a long period where version 2 was the standard, but version 3 is now the recommended release, offering full TypeScript support and a modern Promise API. Ensure you install formidable@latest to get these benefits.
| Feature | busboy | multer | express-fileupload | formidable | connect-busboy |
|---|---|---|---|---|---|
| Primary Use | Custom Streaming | Express Standard | Quick Express Setup | Agnostic / Modern | Legacy Express |
| Data Handling | Streams (Manual) | Streams (Auto) | Buffer/Temp File | Stream or Buffer | Streams (Manual) |
| Ease of Use | Hard (Boilerplate) | Medium | Easy | Medium | Medium |
| Framework | Any | Express Only | Express Only | Any | Connect/Express |
| Configurability | High (Code-based) | High (Object-based) | Low | High | High (Code-based) |
If you are building a standard Express API, reach for multer. It is the battle-tested standard that handles streaming, security limits, and storage configuration without forcing you to reinvent the wheel.
If you need to support multiple frameworks (like Koa or raw http) or want the most modern async/await syntax, formidable (v3) is your best bet. It offers great flexibility without locking you into the Express ecosystem.
Use busboy directly only if you are building a highly custom high-performance service where you need to pipe streams directly to S3 or another service without touching the disk.
Avoid connect-busboy for new development; it is a solution to a problem that multer has already solved better. Similarly, reserve express-fileupload for quick prototypes where strict stream control isn't critical.
Choose busboy if you need maximum performance and control without the overhead of a framework-specific wrapper. It is ideal for custom server implementations (like raw Node.js http or Fastify) where you want to stream data directly to cloud storage or process it on the fly. Be prepared to write more boilerplate code to handle events and streams manually.
Choose connect-busboy only if you are maintaining a legacy Express or Connect application that specifically requires the raw busboy streaming behavior but needs it wrapped as standard middleware. For new projects, prefer multer for better ergonomics or busboy directly for custom setups, as this package adds little value over the others.
Choose express-fileupload if you want the quickest setup for standard file uploads in an Express app without defining storage engines or complex filters. It is perfect for prototypes, microservices, or apps where saving files to a temporary local path before moving them is acceptable. Avoid it for high-throughput systems requiring fine-grained stream control.
Choose formidable if you need a framework-agnostic solution that works across different Node.js servers (not just Express) or if you prefer the modern async/await API introduced in version 3. It is excellent for applications that need to parse both files and standard form fields with equal ease and require strong typing support.
Choose multer if you are building a production-grade Express application that needs robust features like file type filtering, size limits, and configurable storage engines (disk or memory). It is the industry standard for Express file handling because it balances ease of use with the powerful streaming capabilities of busboy under the hood.
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.