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

@pronghorn/bodies

v0.1.0

Published

Unified body parsing middleware for Pronghorn. Handles JSON, urlencoded, and multipart/form-data bodies through one consistent context.locals.body/files API, built on Bun's native FormData support.

Downloads

17

Readme

Bodies 📦

Bodies is a lightweight, TypeScript-first body parsing middleware built as an external plugin for Pronghorn. It unifies JSON, urlencoded, and multipart/form-data request bodies behind a single context.locals.body/context.locals.files API, built directly on Bun's native FormData support.

Built as a standalone package (@pronghorn/bodies), fills the file-upload gap left by Pronghorn's schema validation, which only ever parsed JSON.

Why Bodies

Pronghorn's validate() middleware calls request.json() directly, meaning there was no way to receive file uploads, form-encoded submissions, or mixed text+file payloads. Bodies fixes that without hand-rolling a multipart parser, since Bun already implements Request.formData() natively.

  • One middleware handles application/json, application/x-www-form-urlencoded, and multipart/form-data transparently.
  • Uploaded files are wrapped in an UploadedFile helper with a one-line .save(path) method, no manual ArrayBuffer juggling.
  • Repeated field names (checkboxes, multi-file inputs) are automatically grouped into arrays.
  • Configurable maxSize, maxFileSize, and allowedMimeTypes guard against oversized or unexpected uploads.
  • GET/HEAD requests are skipped automatically since they carry no body.
  • Zero runtime dependencies, pronghorn is only a peer dependency for types.

Installation

bun add @pronghorn/bodies

Requires Bun >=1.3.0 and pronghorn >=0.1.2 as a peer dependency (used for typing the middleware only).

Quick Start

import { createApp, badRequest } from 'pronghorn'
import { bodyParser } from '@pronghorn/bodies'

const app = createApp()

app.use(bodyParser({ maxSize: 10 * 1024 * 1024, maxFileSize: 5 * 1024 * 1024 }))

app.post('/upload', async context => {
  const { avatar } = context.locals.files as { avatar?: UploadedFile }
  const { username } = context.locals.body as { username: string }

  if (!avatar) throw badRequest('Missing avatar file')

  await avatar.save(`./uploads/${username}-${avatar.name}`)
  return context.json({ saved: avatar.name, size: avatar.size })
})

await app.listen(4000)

Core Concepts

JSON bodies

Detected via Content-Type: application/json, parsed with the same fallback-to-{} behavior as Pronghorn's built-in validate() middleware.

app.post('/users', context => {
  const { name, email } = context.locals.body as { name: string; email: string }
  return context.json({ created: { name, email } }, 201)
})

Urlencoded bodies

Detected via Content-Type: application/x-www-form-urlencoded, parsed through the same code path as multipart since both resolve through Bun's FormData API.

app.post('/subscribe', context => {
  const { email } = context.locals.body as { email: string }
  return context.json({ subscribed: email })
})

Multipart bodies and file uploads

Detected via Content-Type: multipart/form-data. Text fields land in context.locals.body; file fields land in context.locals.files, each wrapped as an UploadedFile.

app.post('/documents', async context => {
  const { title } = context.locals.body as { title: string }
  const { document } = context.locals.files as { document?: UploadedFile }

  if (!document) throw badRequest('Missing document')

  await document.save(`./storage/${document.name}`)
  return context.json({ title, fileName: document.name, size: document.size })
})

Multiple files with the same field name

Repeated <input type="file" multiple> submissions (or repeated form keys in general) are grouped into an array automatically.

app.post('/gallery', async context => {
  const { photos } = context.locals.files as { photos?: UploadedFile | UploadedFile[] }
  const list = Array.isArray(photos) ? photos : photos ? [photos] : []

  for (const photo of list) await photo.save(`./gallery/${photo.name}`)
  return context.json({ uploaded: list.length })
})

Size and type limits

Limits are enforced before your handler runs; violations throw a 400 Bad Request via Pronghorn's HttpError, caught by the standard errorHandler middleware.

app.use(bodyParser({
  maxSize: 20 * 1024 * 1024,          // total request body cap
  maxFileSize: 5 * 1024 * 1024,        // per-file cap
  allowedMimeTypes: ['image/png', 'image/jpeg']
}))

Middleware Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | maxSize | number | — | Maximum total request body size in bytes, checked against Content-Length | | maxFileSize | number | — | Maximum size in bytes for any individual uploaded file | | allowedMimeTypes | string[] | — | Whitelist of accepted MIME types for uploaded files |

API Reference

bodyParser(options?: BodyParserOptions): Middleware — global middleware factory, register via app.use(bodyParser(options)).

| Locals property | Type | Description | | --- | --- | --- | | context.locals.body | Record<string, string \| string[]> \| unknown | Parsed text fields (JSON object, or form fields) | | context.locals.files | Record<string, UploadedFile \| UploadedFile[]> | Parsed file fields from multipart bodies |

UploadedFile

| Property/Method | Type | Description | | --- | --- | --- | | name | string | Original filename supplied by the client | | type | string | MIME type reported by the client | | size | number | File size in bytes | | blob | Blob | The underlying File/Blob object, for streaming or manual processing | | save(destinationPath) | Promise<number> | Writes the file to disk via Bun.write, returns bytes written |

Lower-level building blocks are also exported for advanced use outside the middleware:

import { parseBody, wrapUploadedFile } from '@pronghorn/bodies'

const { body, files } = await parseBody(request, { maxFileSize: 5 * 1024 * 1024 })

Architecture

Bodies is split into three modules, each with a single responsibility.

| Module | Responsibility | | --- | --- | | parser.ts | Content-type dispatch (JSON vs. urlencoded/multipart), grouping repeated fields into arrays, enforcing size/type limits | | files.ts | Wraps native File objects from FormData entries into the UploadedFile shape with a .save() helper backed by Bun.write | | middleware.ts | Wires parsing into context.locals.body/context.locals.files, skips GET/HEAD, and converts parsing errors into HttpError(400) |

Both urlencoded and multipart bodies are parsed through the same request.formData() call, since Bun's implementation transparently supports both encodings under one API.

Security Notes

  • Always set maxSize and maxFileSize in production, unbounded uploads are a denial-of-service vector.
  • Use allowedMimeTypes whenever you know the expected file types (e.g. avatar uploads should only accept image types).
  • UploadedFile.save() writes exactly to the path you provide, sanitize document.name before using it in a destination path to avoid path traversal.
  • Parsing errors are surfaced as 400 Bad Request through Pronghorn's HttpError, never silently swallowed.

License

WTFPL (Do What the Fuck You Want to Public License), see LICENSE for details.