busboy vs connect-busboy vs connect-multiparty vs express-fileupload vs formidable vs multer
Handling File Uploads and Multipart Forms in Node.js
busboyconnect-busboyconnect-multipartyexpress-fileuploadformidablemulterSimilar Packages:

Handling File Uploads and Multipart Forms in Node.js

These libraries solve the complex problem of parsing multipart/form-data requests, which is the standard format browsers use when submitting forms with files. While native Node.js streams can handle raw data, these packages abstract the difficult logic of parsing boundaries, decoding headers, and managing temporary storage. busboy is the low-level engine used by many others, while multer, formidable, and express-fileupload are higher-level tools designed for specific frameworks like Express. Some older options like connect-multiparty are deprecated and should be avoided in modern architectures.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
busboy03,003124 kB39--
connect-busboy01564.57 kB1--
connect-multiparty0348-08 years agoMIT
express-fileupload01,559119 kB25a year agoMIT
formidable0-204 kB-a year agoMIT
multer012,07732.8 kB2892 months agoMIT

Node.js File Uploads: A Deep Dive into Busboy, Multer, Formidable, and More

Handling file uploads in Node.js is one of those tasks that looks simple until you realize the HTTP protocol sends files as a continuous stream of binary data mixed with text boundaries. You cannot just "read" the request body like a normal JSON payload. You need a parser that understands multipart/form-data.

The ecosystem offers several tools for this, ranging from low-level stream parsers to high-level Express middleware. Let's break down how they work, where they shine, and which ones you should avoid.

🚨 Critical Warning: Deprecated Packages

Before writing any code, we must address connect-multiparty.

connect-multiparty is deprecated.

The maintainers have explicitly marked it as unmaintained. It does not receive security updates, and its underlying dependencies are outdated. Using it in a new project introduces unnecessary risk. If you see this in an older codebase, treat it as technical debt and plan a migration to multer or formidable.

// ❌ DO NOT USE THIS IN NEW PROJECTS
// const multiparty = require('connect-multiparty');
// const multipart = multiparty();

Similarly, connect-busboy is a thin wrapper around busboy specifically for the old connect middleware stack. While not strictly "broken," it adds little value in modern Express applications where you can use busboy directly or via multer. We will focus on the active, recommended tools.

⚙️ The Engine vs. The Middleware

To understand these packages, you need to distinguish between the parser engine and the middleware wrapper.

  • The Engine (busboy, formidable): These libraries do the heavy lifting. They read the raw HTTP stream, find the file boundaries, decode the binary data, and emit events when a file or field is found. They are framework-agnostic.
  • The Middleware (multer, express-fileupload): These wrap an engine (usually busboy or formidable) to make it easy to use with Express. They handle saving files to disk, renaming them, and attaching the result to req.file so you don't have to manage streams manually.

📦 1. Busboy: The High-Performance Engine

busboy is the gold standard for performance. It is a streaming parser that processes data as it arrives, meaning you can start uploading a 1GB file to S3 before the user finishes uploading it from their browser. It does not save files to disk unless you tell it to.

When to use: Custom middleware, streaming to cloud storage, or non-Express servers.

import busboy from 'busboy';
import { pipeline } from 'stream/promises';
import { createWriteStream } from 'fs';

export function uploadHandler(req, res) {
  const bb = busboy({ headers: req.headers });

  bb.on('file', (name, file, info) => {
    const { filename, encoding, mimeType } = info;
    // Stream directly to disk or cloud without buffering
    const saveTo = createWriteStream(`/tmp/${filename}`);
    
    pipeline(file, saveTo)
      .then(() => res.send('Upload successful'))
      .catch(err => res.status(500).send(err.message));
  });

  req.pipe(bb);
}

Trade-off: You must write the logic to handle file events, errors, and saving mechanisms yourself. It is powerful but verbose.

🧱 2. Multer: The Express Standard

multer is built on top of busboy. It is the most popular choice for Express applications because it solves the "boilerplate problem." Instead of managing streams, you define a "storage engine," and Multer handles the rest. It automatically populates req.file or req.files.

When to use: Standard Express APIs where files need to be saved to disk or memory temporarily.

import multer from 'multer';
import { diskStorage } from 'multer';

// Configure storage
const storage = diskStorage({
  destination: (req, file, cb) => cb(null, 'uploads/'),
  filename: (req, file, cb) => cb(null, `${Date.now()}-${file.originalname}`)
});

const upload = multer({ storage });

// Usage in route
app.post('/profile', upload.single('avatar'), (req, res) => {
  // File is saved and info is ready
  res.json({ filename: req.file.filename });
});

Trade-off: It is tightly coupled to Express. While you can configure it to store in memory (memoryStorage), its primary design pattern assumes a disk-based workflow.

🧩 3. Formidable: The Flexible Generalist

formidable is a robust parser that can work with or without Express. It is famous for its ability to handle both files and regular form fields seamlessly. Unlike Multer, which focuses heavily on files, Formidable treats files and fields as equal parts of the incoming form.

When to use: When you need progress tracking, complex form parsing, or a framework-agnostic solution that is easier to use than raw busboy.

import { IncomingForm } from 'formidable';

export function uploadHandler(req, res) {
  const form = new IncomingForm();

  form.parse(req, (err, fields, files) => {
    if (err) return res.status(500).json({ error: err.message });
    
    // 'fields' contains text inputs, 'files' contains file objects
    res.json({ 
      username: fields.username, 
      photoPath: files.photo.filepath 
    });
  });
}

Trade-off: By default, it saves files to a temporary directory. You often need to manually move the file to its permanent location after parsing, whereas Multer can do this during the upload.

🚀 4. Express-Fileupload: The Simplest Option

express-fileupload is a lightweight wrapper around busboy designed for one thing: simplicity. It requires almost no configuration. You install it, add the middleware, and your files appear on req.files. It is less feature-rich than Multer but significantly faster to set up.

When to use: Small projects, prototypes, or when you want zero-config file uploads in Express.

import fileUpload from 'express-fileupload';
import express from 'express';

const app = express();
app.use(fileUpload());

app.post('/upload', (req, res) => {
  if (!req.files || Object.keys(req.files).length === 0) {
    return res.status(400).send('No files were uploaded.');
  }

  // Move the file manually
  req.files.sampleFile.mv('/somewhere/on/your/server/filename.txt', (err) => {
    if (err) return res.status(500).send(err);
    res.send('File uploaded!');
  });
});

Trade-off: It lacks the advanced "storage engine" configuration of Multer. If you need complex filename logic or strict size limits per file type, you might find yourself fighting its defaults.

🥊 Head-to-Head: Key Technical Differences

1. Streaming vs. Buffering

Performance matters when dealing with large files.

  • busboy is purely streaming. It never buffers the whole file in RAM unless you tell it to. This is critical for large video uploads.
  • multer inherits this streaming capability from busboy.
  • express-fileupload also streams but simplifies the API so much that developers sometimes accidentally buffer data if they misuse the .mv() function on very large files without streams.
  • formidable streams by default but creates temporary files on disk, which involves I/O overhead.
// Busboy/Multer: True streaming (Memory efficient)
// Data flows: Request Stream -> Parser -> Destination Stream

// Formidable: Stream to Temp File -> Move to Final Destination
// Involves extra disk write/read cycle unless handled manually

2. Handling Multiple Files

Different packages structure multiple files differently, which can break your frontend integration if you aren't careful.

// Multer: Array of files on req.files
// If using upload.array('photos'), req.files is [{...}, {...}]

// Express-Fileupload: Object of arrays
// req.files.photos is an array if multiple uploaded with same name

// Formidable: Object where values are File objects (or arrays)
// files.photos might be a single File object or an array depending on config

3. Accessing Form Fields (Text Data)

Often you upload a file along with a caption or user ID. How do you get that text data?

// Multer: Text fields are in req.body
app.post('/', upload.single('pic'), (req, res) => {
  console.log(req.body.caption); // Easy access
});

// Formidable: Text fields are in the 'fields' argument of the callback
form.parse(req, (err, fields, files) => {
  console.log(fields.caption); 
});

// Busboy: You must listen to the 'field' event separately
bb.on('field', (name, val) => {
  console.log(`Field ${name}: ${val}`);
});

📊 Summary Comparison Table

Featurebusboymulterformidableexpress-fileupload
TypeLow-level ParserExpress MiddlewareParser + MiddlewareExpress Middleware
FrameworkAnyExpress OnlyAny (works with Express)Express Only
Ease of UseHard (Manual streams)Easy (Configurable)Medium (Callback based)Very Easy (Zero config)
StorageYou decideDisk or MemoryTemp Disk (default)You decide (via .mv())
Performance⭐⭐⭐⭐⭐ (Best)⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
MaintenanceActiveActiveActiveActive

💡 Final Recommendation

Your choice depends entirely on your project's architecture:

  1. Building a standard Express API? Use multer. It is the community standard for a reason. The storage engine API makes it easy to handle filenames, limits, and destinations cleanly. It strikes the best balance between power and ease of use.

  2. Need maximum speed or custom streaming? Use busboy. If you are uploading directly to AWS S3, Google Cloud Storage, or processing video streams on the fly, busboy gives you the direct access to the stream you need without the overhead of temporary files.

  3. Need simplicity for a small tool? Use express-fileupload. If you just need to get a file from point A to point B in an Express app and don't care about custom storage engines, this will save you time.

  4. Working outside Express or need complex form parsing? Use formidable. It is incredibly reliable for parsing mixed data (files + many text fields) and works well in vanilla Node.js or other frameworks like Koa.

  5. Avoid connect-multiparty at all costs. It is dead software.

By choosing the right tool, you ensure your application handles uploads securely, efficiently, and without blocking your event loop.

How to Choose: busboy vs connect-busboy vs connect-multiparty vs express-fileupload vs formidable vs multer

  • busboy:

    Choose busboy if you need maximum performance and control without framework-specific dependencies. It is the best choice for building custom middleware, handling streaming uploads directly to cloud storage (like S3) without saving to disk, or working with non-Express servers. Be prepared to write more boilerplate code to manage file streams and events manually.

  • connect-busboy:

    Choose connect-busboy only if you are maintaining a legacy application built on the older connect framework that specifically requires this wrapper. For any new project or modern Express setup, you should skip this and use busboy directly or a higher-level alternative, as this package adds an unnecessary abstraction layer that is rarely needed today.

  • connect-multiparty:

    Do NOT choose connect-multiparty for any new project. This package is officially deprecated and unmaintained. It relies on outdated patterns and lacks modern security patches. If you encounter this in an existing codebase, plan to migrate to multer or formidable immediately to ensure security and compatibility.

  • express-fileupload:

    Choose express-fileupload if you want the simplest possible setup for standard Express applications. It requires zero configuration for basic use cases, automatically saving files to a temp directory and attaching them to the request object. It is ideal for small-to-medium projects where you need to get file uploads working quickly without defining complex storage engines.

  • formidable:

    Choose formidable if you need a robust, framework-agnostic parser that excels at handling both file uploads and standard form fields simultaneously. It is particularly strong when you need to parse progress events, handle complex multipart structures, or work outside of the Express ecosystem. It offers a good balance between ease of use and detailed control over the parsing process.

  • multer:

    Choose multer if you are building a production-grade Express application that requires strict control over where and how files are saved. It is the industry standard for Express because of its powerful "storage engine" system, which allows you to easily define custom filenames, limit file sizes, and switch between disk storage and memory storage with minimal code changes.

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.