busboy、express-fileupload、formidable、multer はすべて Node.js アプリケーションで multipart/form-data フォーム(主にファイルアップロード)を処理するためのライブラリです。これらは HTTP リクエストボディからファイルやフィールドデータを解析し、開発者がファイルを保存・処理できるようにします。busboy は低レベルなストリームベースのパーサーで、他の多くのライブラリの基盤となっています。express-fileupload と multer は Express.js 用のミドルウェアとして設計されており、使いやすさに重点を置いています。formidable は長く使われてきたスタンドアロンのフォームパーサーで、Express 以外の環境でも利用可能です。
Node.js でファイルアップロードを実装する際、multipart/form-data の解析は避けて通れません。この分野には複数の選択肢がありますが、それぞれ設計思想や用途が異なります。ここでは、4つの主要ライブラリ — busboy、express-fileupload、formidable、multer — を、実際のコードを交えながら深く比較します。
busboy は、他のライブラリの多くが内部で使っている 低レベルなストリームパーサー です。直接使うと設定が面倒ですが、最大の柔軟性とパフォーマンスを提供します。
// busboy: 生の Node.js HTTP サーバーで使用
const http = require('http');
const Busboy = require('busboy');
const server = http.createServer((req, res) => {
if (req.method === 'POST' && req.headers['content-type']?.includes('multipart')) {
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, filename) => {
// ストリームとしてファイルを受け取る
file.pipe(require('fs').createWriteStream(`./uploads/${filename}`));
});
busboy.on('finish', () => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Upload complete');
});
req.pipe(busboy);
}
});
express-fileupload と multer は、Express 専用のミドルウェア として設計されています。設定が簡単で、すぐに使い始められます。
// express-fileupload
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
app.use(fileUpload());
app.post('/upload', (req, res) => {
// req.files にファイルが格納される
const file = req.files.myFile;
file.mv(`./uploads/${file.name}`, (err) => {
if (err) return res.status(500).send(err);
res.send('File uploaded!');
});
});
// multer
const express = require('express');
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
const app = express();
app.post('/upload', upload.single('myFile'), (req, res) => {
// req.file にファイル情報が入る
res.send(`File saved as ${req.file.path}`);
});
formidable は フレームワーク非依存 のライブラリで、Express でも Koa でも生の HTTP サーバーでも使えます。
// formidable
const express = require('express');
const formidable = require('formidable');
const app = express();
app.post('/upload', (req, res) => {
const form = new formidable.IncomingForm();
form.parse(req, (err, fields, files) => {
if (err) return res.status(500).send(err);
// files にファイル情報が入る
res.send(`File saved at ${files.myFile[0].filepath}`);
});
});
各ライブラリのファイル保存戦略は大きく異なります。
busboy: ファイルを ストリームとして直接処理 するため、メモリに保持せずディスクや外部サービスに即時書き込み可能。メモリ使用量が最小。
express-fileupload: デフォルトでは メモリにファイルを保持(req.files.myFile.data が Buffer)。useTempFiles: true を指定すれば一時ファイルに保存可能。
// express-fileupload with temp files
app.use(fileUpload({ useTempFiles: true, tempFileDir: '/tmp/' }));
formidable: デフォルトで 一時ディレクトリにファイルを保存。keepExtensions: true で元の拡張子を維持可能。// formidable with original extensions
const form = new formidable.IncomingForm({ keepExtensions: true });
multer: ディスク保存がデフォルト(dest オプション指定時)。memoryStorage() を使えばメモリ保存も可能。さらに、カスタムストレージエンジン を実装して AWS S3 などに直接保存できる。// multer with custom storage (e.g., S3)
const storage = multer.memoryStorage(); // or implement your own
const upload = multer({ storage });
プロダクション環境では、ファイルサイズ、MIME タイプ、フィールド名の制限が必須です。
busboy は制限機能を 自分で実装 する必要があります。例えば、ファイルサイズを監視するにはストリームの data イベントでバイト数をカウントします。
express-fileupload は limits オプションで簡易的な制限が可能ですが、MIME タイプのバリデーションはサポートしていません。
// express-fileupload limits
app.use(fileUpload({
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
}));
formidable は maxFileSize や maxFields などのオプションを提供しますが、MIME タイプのチェックは含まれません。
// formidable limits
const form = new formidable.IncomingForm({
maxFileSize: 5 * 1024 * 1024,
maxFields: 10
});
multer は 最も包括的なバリデーション機能 を持っています。fileFilter オプションで MIME タイプやファイル名をチェックでき、エラー時にカスタムレスポンスも返せます。
// multer with file validation
const upload = multer({
dest: 'uploads/',
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('Only images allowed!'), false);
}
},
limits: { fileSize: 5 * 1024 * 1024 }
});
// エラーハンドリング
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
res.status(400).send('Multer error');
} else {
res.status(500).send(err.message);
}
});
busboy: 純粋な Node.js ライブラリなので、どのフレームワークでも使用可能。express-fileupload: Express 専用。他のフレームワークでは使えません。formidable: フレームワーク非依存。Koa、Fastify、生の HTTP サーバーなど、どこでも動作。multer: Express 専用。ただし、multer のコアロジックは busboy に依存しているため、理論的には他の環境でも再利用可能ですが、公式サポートはありません。express-fileuploadbusboy または multer + カスタムストレージformidablemulter| 機能 | busboy | express-fileupload | formidable | multer |
|---|---|---|---|---|
| 対応フレームワーク | 任意 | Express 専用 | 任意 | Express 専用 |
| メモリ効率 | ⭐⭐⭐⭐⭐ (ストリーム) | ⭐⭐ (デフォルトでメモリ) | ⭐⭐⭐⭐ (一時ファイル) | ⭐⭐⭐⭐ (カスタム可) |
| バリデーション機能 | ❌ (自前実装) | ⭐ (サイズ制限のみ) | ⭐⭐ (サイズ・フィールド数) | ⭐⭐⭐⭐⭐ (MIME含む) |
| カスタムストレージ | ⭐⭐⭐⭐⭐ (自由自在) | ❌ | ⭐⭐ (限定的) | ⭐⭐⭐⭐⭐ (公式サポート) |
| 学習コスト | 高 | 低 | 中 | 中 |
express-fileuploadmulterformidable または busboybusboyこれらのライブラリは目的が異なるため、「どれが一番良い」というより「どの要件に最も合うか」で選ぶのが正解です。特に、ファイルアップロードはセキュリティリスクが高い領域なので、バリデーションとエラーハンドリングを疎かにしないよう注意してください。
busboy は、細かい制御が必要なカスタム実装や、既存の高レベルライブラリでは要件を満たせない場合に選ぶべきです。ストリーム処理によるメモリ効率が高く、大規模なファイルアップロードやリアルタイム処理に向いていますが、設定やエラーハンドリングを自分で実装する必要があります。Express 専用ではなく、生の Node.js HTTP サーバーでも使えます。
multer は、Express アプリで堅牢かつ柔軟なファイルアップロード処理を実装したい場合に最適です。ストレージエンジンのカスタマイズ、ファイルサイズ制限、フィールド名のバリデーションなど、プロダクション環境で必要な機能が揃っています。AWS S3 やその他のクラウドストレージとの連携もしやすく、中〜大規模アプリケーションでの採用が多いです。
express-fileupload は、Express アプリで手軽にファイルアップロード機能を追加したい場合に最適です。設定が非常にシンプルで、ファイルをメモリまたは一時ディレクトリに自動保存し、req.files でアクセスできます。ただし、高度なバリデーションやストレージ統合(例: S3 直接保存)には向かず、小〜中規模プロジェクト向けです。
formidable は、Express に依存しない汎用的なフォームパーサーが必要な場合や、複数のフレームワーク間で共通の処理ロジックを使いたい場合に適しています。長い歴史があり安定していますが、API がやや古く感じられ、TypeScript サポートも限定的です。大規模なアップロード処理には対応できますが、Express との統合は multer や express-fileupload ほどスムーズではありません。
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.