busboy vs express-fileupload vs form-data vs formidable vs multer
Handling File Uploads and Multipart Forms in Node.js
busboyexpress-fileuploadform-dataformidablemulterSimilar Packages:

Handling File Uploads and Multipart Forms in Node.js

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.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
busboy03,003124 kB39--
express-fileupload01,559119 kB25a year agoMIT
form-data02,35483.9 kB1402 months agoMIT
formidable0-204 kB-a year agoMIT
multer012,07432.8 kB2882 months agoMIT

Handling File Uploads in Node.js: A Deep Dive into busboy, multer, formidable, and more

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.

🏗️ Architecture Level: Low-Level Streams vs. Ready-Made Middleware

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
});

💾 Storage Strategies: Automatic vs. Manual Control

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);
});

🚦 Flow Control and Limits

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');
});

📡 Progress Tracking and Events

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.

☁️ Use Case Scenarios

Scenario 1: Standard Express API

You are building a REST API with Express and need to save user avatars to disk.

  • Best Choice: multer
  • Why? It integrates perfectly with Express routes, handles disk storage configuration cleanly, and includes safety limits out of the box.

Scenario 2: Microservice or Vanilla HTTP Server

You are using a lightweight framework like Fastify or the native http module and need to parse uploads.

  • Best Choice: formidable or busboy
  • Why? formidable 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.

Scenario 3: Quick Prototype

You need to test a file upload endpoint in 5 minutes.

  • Best Choice: express-fileupload
  • Why? Minimal setup. Just app.use(fileUpload()) and access req.files. No storage config needed for basic tests.

Scenario 4: Uploading to a Third-Party API

Your server needs to forward an uploaded file to an external image processing service.

  • Best Choice: form-data
  • Why? This library is designed to construct the outgoing multipart request. You would pipe the incoming stream from busboy/multer into a form-data instance to forward it.

⚠️ Deprecation and Maintenance Notes

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.

📊 Summary Comparison

Featuremulterformidablebusboyexpress-fileuploadform-data
Primary RoleExpress MiddlewareStandalone ParserStreaming ParserExpress MiddlewareRequest Constructor
FrameworkExpress OnlyAny (Vanilla, Fastify, etc.)AnyExpress OnlyAny (Client-side)
StorageConfigurable (Disk/Memory)Auto-save to DirManual (Stream)Manual (.mv())N/A (Sends data)
Progress EventsNo (Native)YesManualNoN/A
Ease of UseHighMediumLow (Verbose)Very HighHigh
Best ForStandard Express AppsCustom Servers / ProgressHigh Perf / Cloud StreamsPrototypesSending Files

💡 The Big Picture

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.

How to Choose: busboy vs express-fileupload vs form-data vs formidable vs multer

  • busboy:

    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.

  • express-fileupload:

    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.

  • form-data:

    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.

  • formidable:

    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.

  • multer:

    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.

README for busboy

Description

A node.js module for parsing incoming HTML form data.

Changes (breaking or otherwise) in v1.0.0 can be found here.

Requirements

Install

npm install busboy

Examples

  • Parsing (multipart) with default options:
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!
  • Save all incoming files to disk:
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');
});

API

Exports

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.

(Special) Parser stream events

  • 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.