npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

node-multipart

v1.1.3

Published

A simple dependency-free streaming multipart/form-data parser for Node.js.

Downloads

0

Readme

node-multipart

Let's talk about multipart/form-data for a second, because you've probably fought with it before without really seeing it.

You know the shape of the problem: someone submits a form with a text field and a file, and suddenly your server has to deal with a wire format that's part text, part binary, glued together with boundary strings. Most libraries you've reached for handle that by also deciding, on your behalf, what an "upload" is — where it goes, how big it's allowed to be, what happens when the field is small versus huge. node-multipart doesn't do that. It just parses the protocol and hands you the pieces, one at a time, as a stream.

You call it a part. That's the whole vocabulary you need: a part is one item in a multipart request. name=Seyi is a part. A JPEG is a part. A ten-line text file someone dragged in is a part. node-multipart doesn't treat any of these as more special than the others — it just gives you the next one and gets out of your way.

const part = await multipart.nextData()
const bytes = await multipart.pipeNextTo(destination)
const value = await multipart.readNextValue()

That's most of the API, right there.

Why you'd want this

Picture the form you've written a hundred times:

<form enctype="multipart/form-data">
    <input name="name">
    <input type="file" name="avatar">
</form>

Submit it, and you get a request that's conceptually:

name       → "Seyi"
avatar     → JPEG bytes

Somewhere underneath, boundaries are separating those two chunks, and headers are describing each one. You don't want to think about the boundaries. node-multipart thinks about them for you — and stops exactly there.

A file part doesn't have to become a temp file just because you called it a "file." A form field doesn't have to become a JS object just because it's small. You get to decide, per part, what happens to the bytes:

await multipart.pipeNextTo(destination)

destination can be a filesystem stream, a hashing stream, a transform, an adapter for object storage — anything that implements Writable. The parser never asks.

The whole API, in one loop

Here's the shape you'll actually write:

const multipart = new Multipart(request)
let part

while (part = await multipart.nextData()) {
    // Decide what to do with this part.

}

Once you've got a part, it's yours. You inspect its metadata — name, filename, contentType — and decide, using whatever logic your application needs.

One thing worth knowing before you get further in: the parser is strictly sequential. It won't fetch the next part behind your back, and — looking at the source — it actively stops you from trying: nextData() throws if the part you already selected hasn't been consumed yet. That's not an accident, it's the whole design. You either drain a part with pipeNextTo or readNextValue before moving to the next one, or the parser tells you no. It's a low-level primitive, not an upload subsystem trying to guess what you meant.

Streaming a file, for real

import { createWriteStream } from 'node:fs'
import { Multipart } from 'node-multipart'

const multipart = new Multipart(request)
let part

while (part = await multipart.nextData()) {
    if (part.filename) {
        const destination = createWriteStream(
            `/uploads/${crypto.randomUUID()}`
        )

        const bytesWritten = await multipart.pipeNextTo(
            destination,
            10 * 1024 * 1024
        )
        console.log(`stored ${bytesWritten} bytes`)
    }
}

Nothing here holds the whole file in memory. Whatever destination you hand it controls where the bytes actually end up.

Reading the small, ordinary fields

A form field is just a part with no filename. That's it — same object shape, same API:

const part = await multipart.nextData()

if (part && !part.filename) {
    const value = await multipart.readNextValue()
    console.log(part.name, value)
}

readNextValue() isn't magic — under the hood it's a PassThrough fed by pipeNextTo, with the resulting chunks joined into a string. It doesn't scan ahead or collect every field in the request for you; it just drains the part you already selected. If you want the field to stay a stream instead of a string, use pipeNextTo() directly, same as you would for a file.

What nextData() actually gives you

If you're in TypeScript, this comes typed as NextData:

import type { NextData } from 'node-multipart'
type NextData = {
    name: string | null
    filename: string | null
    contentType: string | null
    headers: Record<string, string>
}

A file part looks like:

{
    name: 'avatar',
    filename: 'photo.jpg',
    contentType: 'image/jpeg',
    headers: {
        'content-disposition':
            'form-data; name="avatar"; filename="photo.jpg"',
        'content-type':
            'image/jpeg'
    }
}

And a plain field looks like this — no filename, no content type:

{
    name: 'username',
    filename: null,
    contentType: null,
    headers: {
        'content-disposition':
            'form-data; name="username"'
    }
}

One thing to know if you're dealing with non-ASCII filenames: some clients send filename*=UTF-8''caf%C3%A9.png (the RFC 6266 extended form) instead of, or alongside, a plain filename="...". node-multipart decodes that for you and prefers it over the plain form when both are present, so part.filename comes back as café.png either way.

Let's talk about limits, because you'll need them

node-multipart bounds the things it's actually in a position to bound. It can't know your total request-size policy, but it knows exactly how large the part in front of it is getting, in real time, so it enforces limits there.

Part size. Pass a byte ceiling to pipeNextTo:

await multipart.pipeNextTo(
    destination,
    10 * 1024 * 1024
)

Go over it, and the call rejects instead of quietly continuing to write. You still get back how much was written up to that point via the return value on success:

const bytes = await multipart.pipeNextTo(destination)

That limit is per-part, not per-request — you're still responsible for the request as a whole.

Form-field size. Same idea, smaller usual numbers:

const value = await multipart.readNextValue(
    1024
)

Header size. This one's easy to forget about, but headers are attacker-controlled text too:

const part = await multipart.nextData(
    64 * 1024
)

If you look at the implementation, the header check is actually a little more careful than "count all the bytes in the buffer." It only throws once it's isolated where the header section actually ends — so a big chunk of body data arriving in the same read as the header terminator doesn't get wrongly blamed on the headers. Small detail, but it's the kind of thing that's easy to get lazily wrong, and it isn't here.

You still need your own ceiling on the overall HTTP request. That's not something a multipart parser sitting mid-stream can enforce for you.

Backpressure — the part people skip and shouldn't

pipeNextTo() writes straight into the Writable you gave it, and it respects that writable's backpressure signal. Concretely: if destination.write() returns false, the parser stops pushing and waits on 'drain' before continuing — you can see this in the #write helper wrapping every write in a promise that resolves on drain (or rejects on error).

Practically, that means a slow destination — a rate-limited object-storage upload, a database write, whatever — doesn't force the parser to buffer an entire file in memory while it waits. The incoming request and the outgoing destination form one ordinary, backpressure-respecting Node.js pipeline, the same as any other stream you'd wire up by hand.

await multipart.pipeNextTo(destination)

What can a part become? Genuinely, whatever you want

The parser has no opinion here, and it's not being coy about it. A part could go to:

  • a local file
  • an object-storage adapter
  • a database-backed writable
  • a compression or transformation stream
  • an encryption stream
  • a content-processing pipeline
  • a hashing stream
  • any Writable you write yourself
  • another HTTP or service pipeline downstream

You could build a Writable that forwards chunks straight into an object-storage API's upload stream, and node-multipart would never need to know AWS, Azure, or GCP exist. That's deliberate — it's also why this project has no intention of becoming a storage abstraction. That's a different job.

How this compares to Busboy, Formidable, Multer

Let's be straightforward about this instead of pretending it's a unique category.

At the level of "can this parse multipart data as a stream without buffering the world," node-multipart and Busboy are doing the same job. It would be dishonest to claim otherwise — Busboy streams too.

Where they actually diverge is scope and the shape of the API you're handed. node-multipart stays close to the wire:

await multipart.nextData()
await multipart.pipeNextTo(destination)

There's no field aggregation, no built-in storage strategy, no framework glue, no cloud SDK bundled in. That's not a missing-feature list — those things genuinely differ from project to project, sometimes from upload to upload inside the same project, so baking one answer into the parser would mean fighting it later. node-multipart leaves the decision where it belongs: with you.

The short version — select the next part, look at its metadata, decide where the bytes go.

Building something bigger on top of it

Because the parser doesn't carry a storage or framework opinion, you can build one without touching it:

HTTP request
     │
     ▼
node-multipart
     │
     ├── form field → application
     │
     ├── image → image processing
     │
     ├── document → object storage
     │
     └── other part → custom pipeline

A framework adapter can sit on top. A storage layer can sit on top. Your app-specific upload pipeline can sit on top. None of that needs to leak back down into the parser — and an ecosystem of adapters can live entirely in separate packages, evolving at its own pace, without this project having to grow to match.

It's not only for browser forms

"Multipart parser" tends to get read as "file upload library," and that's fair — it's the common case. But the format itself is just a way of carrying several named chunks of bytes in one request, and that shows up in places that have nothing to do with an <input type="file">. A few things people build with a primitive like this:

  • Streaming straight to object storage. Pipe a part directly into an S3/GCS multipart-upload stream so your server is a pass-through, not a place files land twice.
  • Scanning as data arrives. Pipe into a virus/content-scanning stream and reject mid-upload on a match, instead of scanning a file after it's already fully written.
  • Processing on the fly. Pipe an avatar upload into an image-resizing stream and never store the original at all.
  • Resumable/chunked upload backends. Sequential part consumption plus a destination you fully control is a reasonable foundation for a tus-like protocol.
  • Webhook and legacy integrations. Some webhook providers and older SOAP/MTOM-style integrations use multipart payloads that have nothing to do with a browser form — same parser, same API.
  • Live checksums. Tee a part into crypto.createHash() alongside storage to get an integrity hash without buffering the part twice.

None of this requires anything from the parser beyond what it already does — pipeNextTo into any Writable you like.

Security — the part that's actually on you

node-multipart gives you bounded primitives. It cannot, by itself, make your endpoint safe — that requires decisions only you can make. So, concretely, you should:

  • Set a maximum HTTP request/upload size.
  • Set sensible multipart header limits.
  • Set sensible per-part size limits.
  • Generate your own storage filenames — never trust filename as a path.
  • Validate the fields an endpoint is actually willing to accept.
  • Validate uploaded file types against your own requirements.
  • Handle aborted requests and stream errors.
  • Cap how many parts a single request is allowed to contain.
  • Apply authorization before anything gets persisted.

That second point deserves its own callout, because it's the classic mistake:

// Don't do this.
createWriteStream(`/uploads/${part.filename}`)

filename comes from the request. Treating it as a filesystem path is how you end up with path traversal. Generate your own name instead:

createWriteStream(
    `/uploads/${crypto.randomUUID()}`
)

The parser hands you bounded streaming. What those bounds should actually be, and what you do once a part passes them, stays your call.

The rule this whole project follows

Parse the protocol. Don't decide what the application should do with it.

So node-multipart owns:

  • Multipart framing
  • Boundary detection
  • Part metadata
  • Sequential part consumption
  • Streaming part contents
  • Writable-stream backpressure
  • Part-size limits
  • Header-size limits

And you own:

  • Storage
  • File naming
  • Validation
  • Authorization
  • Request limits
  • Part-count limits
  • Field handling
  • Persistence
  • Whatever's specific to your application

That split is the entire pitch, and it's also why the parser can stay small while whatever you build on top of it grows as complicated as it needs to.

Requirements

  • Node.js 20+
  • IncomingMessage, or any compatible async-iterable request stream
  • No runtime dependencies

License

MIT