form-data: the multipart encoder Node still builds HTTP uploads on
A module to create readable `"multipart/form-data"` streams. Can be used to submit forms and file uploads to other web applications.
At a glance
- What is it?
- A small library that turns strings, buffers and streams into multipart/form-data bodies, with a README that doubles as a catalogue of awkward edge cases.
- Who is it for?
- form-data survives because the problem it solves never went away, and because the awkward parts of multipart are its feature set rather than its bugs. Knowing a stream's length, injecting per-part headers, sending a relative filepath, passing auth in the submit options: those are the cases that show up once you upload a directory of files or talk to an API that wants a relative path.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 117 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 7, 2026, and from our analysis. They are not legal advice.
Editorial analysis
A readable stream, not a buffer
The one-line description in the README is precise about what you get: a library to create readable multipart/form-data streams that can be used to submit forms and file uploads to other web applications. The word readable is doing real work, because the output is a Node stream you can pipe somewhere rather than a string you have assembled in memory.
That distinction is what lets you upload a file without loading it. The basic case appends a string, a buffer and a read stream to one form:
var FormData = require('form-data');
var fs = require('fs');
var form = new FormData();
form.append('my_field', 'my value');
form.append('my_buffer', new Buffer(10));
form.append('my_file', fs.createReadStream('/foo/bar.jpg'));The API is deliberately modeled on the XMLHttpRequest 2 FormData interface, which the README links to the W3C spec for. So if you have written this in a browser, the method names transfer, but the implementation is a Node stream and not a browser object.
Three ways to submit, and why the last one exists
There is a one-liner method, and the README is careful to explain what it does and does not give you. Calling `submit(url, [callback])` posts the form. For more advanced request manipulation it returns an `http.ClientRequest` object instead.
That distinction is the reason the alternative methods exist. If you want to own the request, use Node's http client directly:
var request = http.request({
method: 'post',
host: 'example.org',
path: '/upload',
headers: form.getHeaders()
});
form.pipe(request);`form.getHeaders()` is the piece people forget. The boundary is generated per form instance, so the Content-Type header with its boundary parameter only exists after the form exists, and forgetting to set it produces a server that rejects the upload for reasons that are tedious to diagnose.
If you would rather have the Content-Length header set for you, the README notes you can just call `submit()` with a URL and let the library compute it. That option is not free: computing length means knowing the size of every part in advance, so a stream whose length is unknown has to fall back to chunked transfer.
Length, boundaries and per-part headers for the awkward cases
The advanced section handles a case that comes up when you are generating forms against a specific server. You can pass a `header` and a `knownLength` in the options for a part:
var CRLF = '\r\n';
var form = new FormData();
var options = {
header: CRLF + '--' + form.getBoundary() + CRLF + 'X-Custom-Header: 123' + CRLF + CRLF,
knownLength: 1
};
form.append('my_buffer', buffer, options);The README points at combined-stream for the full list of available options, which is the stream library underneath. One option it shows is `maxDataSize`, passed to the constructor: `new FormData({ maxDataSize: 20971520 })`, which caps how much data the form will hold.
There is also a size floor worth knowing about. Any large-value option like `maxDataSize` exists because the library will buffer up to a threshold before it has to start streaming, and the default for that is small enough that large buffers get written to a temporary file rather than held in memory.
Filenames, relative paths and directory uploads
The library can recognize and fetch the required metadata from common stream types on its own, naming `fs.createReadStream`, an `http.response` and a stream from the request module. For any other stream type you have to provide the file information yourself.
form.append('file', stdout, {
filename: 'unicycle.jpg', // ... or:
filepath: 'photos/toys/unicycle.jpg',
contentType: 'image/jpeg',
knownLength: 19806
});The `filepath` property overrides `filename` and may contain a relative path. The README links this specifically to uploading multiple files from a directory, which is the situation where a server rejects an upload because the filename is not what it expected.
The `knownLength` option in that same block is there for the same reason: without it the library cannot know how big the part is. Note also that `contentType` is set explicitly here, which matters because a generic stream gives the library nothing to infer from.
Query strings, auth and custom headers in the submit options
The last group of examples covers the cases where the URL alone is not enough. Instead of a URL string, you can pass an options object as the first argument to `submit()`:
form.submit({
host: 'example.com',
path: '/probably.php?extra=params',
auth: 'username:password'
}, function (err, res) {
console.log(res.statusCode);
});The README frames these as edge cases: a POST to a URL with a query string, and passing HTTP auth credentials. The second example adds a `headers` key for custom HTTP headers on the POST request, which is how you add a test or trace header without reaching for the pipe form.
This is the ergonomic cost of building multipart by hand. Node's http module will do none of this for you, so every one of these needs an explicit option. The library's value is that the options exist and are documented, not that they are discoverable.
The repository tells you how carefully it is maintained
The `package.json` is more revealing than the README about the state of things. The version is 4.0.6 and the main entry point is `./lib/form_data`, with a separate browser build at `./lib/browser` and TypeScript definitions at `./index.d.ts`. Author credit goes to Felix Geisendörfer, who also wrote the underlying combined-stream library.
The test and lint setup is worth a look. `pretest` runs the lint step, coverage comes from istanbul over `test/run.js`, there is a `check-coverage` script against the coverage JSON, and a separate browser test path that runs browserify with the istanbul transform and pipes output to obake for coverage. The CI scripts are `ci-lint` and `ci-test`, and the lint script is `eslint --ext=js,mjs .`, which means the codebase has already moved to ES modules in at least some places.
There is a small piece of self-maintenance in there too: an `update-readme` script that rewrites the badge URLs to point at the current version tag, so the README badges always describe the release you are reading. The repository metadata shows 2,356 stars and 404 forks, 141 open issues, MIT licensed, last pushed on 2026-06-12, and the default branch is `master` rather than `main`, which dates the project to a period before that convention changed.
Editorial conclusion
form-data survives because the problem it solves never went away, and because the awkward parts of multipart are its feature set rather than its bugs. Knowing a stream's length, injecting per-part headers, sending a relative filepath, passing auth in the submit options: those are the cases that show up once you upload a directory of files or talk to an API that wants a relative path. The README documents all of them, which is more than most libraries of this size manage. Install it, use `form.pipe(request)` when you want to control the request and `submit()` when you do not, and treat the TypeScript definitions as the authority rather than the prose.
Frequently asked questions
What does the form-data npm package do?
It creates readable multipart/form-data streams in Node.js, which you can use to submit forms and file uploads to other web applications. You append strings, buffers and streams to a form, then either pipe it into a request or hand it to the `submit()` method. Its API follows the XMLHttpRequest 2 FormData interface.
How do I install and use form-data in Node?
Run `npm install --save form-data`, require the module, and append your fields. Streams are piped rather than buffered, so you can attach an `fs.createReadStream` directly. Call `form.getHeaders()` and pass the result to your request, since the Content-Type header contains the generated boundary.
Why does my form-data upload fail with a 400 error?
The most common cause is a missing Content-Type header. The boundary is generated per form instance, so you must pass `form.getHeaders()` into the request options, or use `form.submit()` which sets it for you. Other documented causes are an unknown stream length and a missing `filename`, `contentType` or `knownLength` when appending a stream that the library cannot inspect.
Can form-data be used in the browser?
There is a browser build: `package.json` maps the `browser` field to `./lib/browser`, and the CI runs a browser test through browserify with an istanbul transform. The library is still a Node stream implementation, so in a browser the native FormData object is usually the simpler choice.
Does form-data work with TypeScript?
Yes. TypeScript definitions ship with the package at `./index.d.ts`, and the `typings` field in `package.json` points there. The README also links to the method reference on the repository for the full signature list, including `append`, `getHeaders` and `submit`.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/form-data-form-data)