PAS7 Studio

Body parsing and validation in Bun.js: JSON, uploads, streams, and payload limits

A practical deep dive into body parsing middleware in Bun.js: JSON, FormData, uploads, streams, payload limits, schema validation, idempotency, content-type checks, safe middleware order, and bad practices.

14 May 2026· 13 min read· Technology
Best forBackend engineersFull-stack developersTech leadsTeams building JSON APIs, webhook endpoints, or upload flows on Bun
Technical illustration of a body parsing pipeline in Bun.js with JSON, uploads, streams, and validation

In Bun, as in the broader Web API model, the request body is not an infinite variable you can read as many times as you want. It is a stream. If one layer reads it without a contract with the rest of the pipeline, the next layer may receive an empty body, an error, or unexpected behavior.

That is why body parsing should not be hidden in global middleware “just in case”. It must be route-aware, content-type aware, and size-aware.

In this article, we break down how to build parsing and validation so they protect the API instead of creating memory pressure, security holes, and strange bugs.

JSON, FormData, uploads, and streams need different pipelines.
Payload limits must run before expensive parsing, not after it.
Schema validation should create typed parsed context, not just “check something”.
Uploads need a security policy separate from the JSON API.

This is the final chapter in the current Bun middleware roadmap. It closes the topic of request bodies, which are one of the most common sources of memory, security, and validation problems in APIs.

The mental model of body ownership: who reads the body, who validates it, and who passes the parsed value forward.
JSON middleware: content-type, size limit, parsing, schema validation, typed context.
FormData and uploads: multipart, file size, extension/MIME allowlist, storage, malware scanning. [4]
Streams and large payloads: when you cannot buffer everything in memory.
Idempotency keys for POST/PUT flows that may be retried.
Bad practices: global req.json(), body logs, parsing after auth/rate limit in the wrong order, validation without strip/strict policy.

The most important rule: the request body should have one owner. If multiple middleware layers try to read the body independently, the pipeline becomes fragile.

01

Check route and method

Do not read the body for GET/HEAD or routes that do not need a body. This saves latency and removes accidental side effects.

02

Check content-type

A JSON route should accept application/json; an upload route should accept multipart/form-data or a specific binary content-type. Do not parse everything as JSON.

03

Apply size limit

If the limit is checked after await req.json() or await req.formData(), it no longer protects memory from a large payload.

04

Read the body once

Use json(), formData(), arrayBuffer(), or a stream depending on the route. Pass the parsed value into context.

05

Validate and pass typed value

Schema validation should return a typed result or a stable 400/422, not leave the handler guessing.

Summary

Body ownership makes the pipeline predictable: the handler receives a ready parsed value instead of deciding what happened to the stream.

The Bun HTTP server works with the Web Request/Response model. That means the basic body-reading methods are familiar from the Fetch API: json(), text(), formData(), arrayBuffer(), blob(), and stream. [1][2]

MethodWhen to useWhat it returnsMain caution
req.json()JSON API, JSON webhooks, config mutationsParsed JavaScript valueFails on invalid JSON and needs size/content-type/schema checks
req.formData()Forms, multipart uploads, mixed text/file fieldsFormData with fields/filesMay buffer large uploads, so file limits and storage policy are needed
req.arrayBuffer()Binary payloads, signatures, raw webhook verificationArrayBufferCan sharply increase memory pressure on large payloads
req.text()Plain text, raw signature input, small text payloadsStringNot suitable for large streams or uploads
req.body streamLarge payloads, streaming ingestion, custom parsersReadableStream or nullYou must control backpressure, limits, and errors yourself

The body API should be chosen per route. JSON, FormData, binary, and streams should not be sent through one universal parser.

Section request-body-apis screenshot

Summary

A universal body parser is convenient at the start, but production APIs should be separated by content-type and route purpose.

For JSON routes, you need more than await req.json(). You need a small gate: check content-type, approximate content-length, read the body, handle parse errors, validate schema, and pass a typed result forward.

A minimal native Bun pattern:

TS
type JsonContext<T> = { body: T };
type JsonHandler<T> = (req: Request, ctx: JsonContext<T>) => Promise<Response> | Response;

function jsonRoute<T>(schema: { parse: (value: unknown) => T }, handler: JsonHandler<T>) {
  return async (req: Request) => {
    const contentType = req.headers.get("content-type") ?? "";
    if (!contentType.includes("application/json")) {
      return Response.json({ error: "unsupported_media_type" }, { status: 415 });
    }

    const contentLength = Number(req.headers.get("content-length") ?? 0);
    if (contentLength > 256_000) {
      return Response.json({ error: "payload_too_large" }, { status: 413 });
    }

    try {
      const raw = await req.json();
      const body = schema.parse(raw);
      return handler(req, { body });
    } catch {
      return Response.json({ error: "invalid_request_body" }, { status: 400 });
    }
  };
}

In real code, it is better to separate parse errors from validation errors so you can return 400 for invalid JSON and 422 for schema mismatch, if your API contract supports it. But the key point is that the handler no longer reads the body itself.

Practical rule

JSON middleware should create typed body context. If the handler calls req.json() again, ownership is not working.

File upload is one of the riskiest API surfaces. The OWASP File Upload Cheat Sheet recommends limiting extensions, validating content type, changing filenames, setting size limits, storing files outside the webroot, checking permissions, and scanning files where needed. [4]

In Bun, an upload route should not go through the same JSON middleware. Uploads need separate rules: what can be uploaded, what the maximum is, where to store it, how to name it, when to scan it, and whether asynchronous processing is needed.

It is important to separate metadata validation from file validation. Form fields can be validated with a schema, but the file needs separate checks: size, declared MIME, magic bytes, extension allowlist, malware scanning, storage path.

Do not trust the original filename.
Do not store uploads in a public webroot unless you have a clear reason.
Do not accept any MIME type or extension.
Do not read large files into memory if they can be streamed into storage.
Do not log file contents or sensitive metadata.

An upload route needs its own pipeline: size limit, type allowlist, scan/validation, and a safe storage decision.

Section uploads screenshot

If a route accepts a large payload, ingestion stream, media upload, or long-running transfer, arrayBuffer() and formData() may be the wrong choice. They are convenient, but they can force the runtime to keep a significant amount of data in memory.

A streaming flow requires a different mindset: early limits, backpressure, abort handling, timeout policy, storage streaming, partial failures, and cleanup. It is not “a normal JSON endpoint with a large body”.

Observability is different for streams too: total request time may be long and normal, so it is important to measure throughput, bytes accepted, time to first byte, storage errors, and client aborts.

Large payloads should be streamed through the pipeline, not turned into one large buffer just to make the handler convenient.

Section streams screenshot

Summary

Once a payload can be large, body parsing becomes a performance and reliability topic, not just a validation helper.

Schema validation should not only check types. It should define what happens to unknown fields, coercion, defaults, and partial updates.

Strict schema

Good for security-sensitive APIs: unknown fields are rejected. The downside is that clients may break when they send extra fields.

Strip unknown

Convenient for public APIs: extra fields are removed, and business logic sees a clean shape. Be careful when logging schema mismatches.

Passthrough

Useful only when the API truly accepts dynamic payloads. Otherwise, it creates a risk of mass assignment or hidden fields.

Coercion everywhere

Automatic type conversion can be convenient, but it sometimes hides client bugs. For money, permissions, and security flags, it is better to be stricter.

Summary

Validation policy should be part of the API contract. If every route treats unknown fields differently, clients and security review quickly lose predictability.

POST/PUT flows involving payments, orders, webhook retries, file ingestion, and background jobs have a separate problem: the client may retry the request because it did not receive a response or got a timeout. Body validation should work together with an idempotency key here.

Idempotency middleware usually checks Idempotency-Key, route group, subject/tenant, and body hash. If the same key arrives with a different body, that should be a conflict, not a second execution of the mutation.

This is not required for every JSON route, but for expensive or irreversible operations it is often cheaper than untangling duplicate business data after an incident.

Practical rule

If it is dangerous to execute a mutation twice, body validation should include an idempotency story.

These mistakes most often appear when a team tries to build one “universal body middleware” for the whole API.

Global await req.json() for all routes.

Reading the body in multiple middleware layers without passing parsed context.

Payload limit after parsing instead of before it.

One parser for JSON, multipart, binary, and streams.

Logging raw body in production.

Missing 415 Unsupported Media Type for incorrect content-type.

Uploads stored with the original filename in a public directory.

Validation allows unknown fields in security-sensitive endpoints.

No distinction between invalid JSON (400) and schema mismatch (422) when the API contract needs it.

Large payload routes have no timeout, abort, and cleanup policy.

Review rule

A body parser should be route-specific. If it “works for everything”, it almost certainly does not protect important edge cases.

Before launching a Bun API in staging or production, go through this list for each endpoint class: JSON, upload, stream, webhook, and mutation.

Body owner is defined

One middleware or route-level gate reads the body and passes parsed context forward.

Content-type is checked

JSON, multipart, binary, and streams have different accepted content-types.

Size limit runs early

The limit is applied before parsing or during streaming ingestion, not after the full buffer.

Schema validation has a policy

Strict, strip, or passthrough behavior is defined and consistent for the route class.

Uploads have security controls

Extension/MIME allowlist, size limits, safe filename, storage policy, permissions, and scanning where needed.

Streams have abort/cleanup

Client abort, timeout, partial writes, and storage errors are handled explicitly.

Errors are stable

400, 413, 415, 422, and 500 have a predictable JSON contract.

Body is not logged

Logs include request id, route, status, validation error class, but not raw body or file contents.

Bun gives you a convenient Web Request/Response model, but that is exactly why it is easy to forget that the body is a stream, not a regular object. Good middleware does not read the body “just in case”. It knows the route, method, content-type, size limit, and schema.

JSON, uploads, streams, and binary payloads need different pipelines. In production, a universal parser often becomes a source of memory pressure, validation ambiguity, and security risk.

The best approach is simple: one body owner, early limit, correct parser, typed validation result, stable error contract, and separate upload/stream policy. This closes most issues before the handler is reached.

Can I read `req.json()` in multiple middleware layers?

It is better not to. The request body is a stream, and it should be read once by the responsible layer. Pass the parsed value into context or the handler. Re-reading without a contract creates fragile bugs.

Where should the payload limit be applied?

As early as possible: before expensive parsing or during streaming ingestion. If you first run `await req.json()` or `await req.formData()`, a large payload has already entered memory.

Can one middleware handle both JSON and uploads?

For production, usually no. JSON, multipart uploads, binary payloads, and streams have different security, memory, and validation requirements. They should be separated by route classes.

What should an API return for an invalid body?

`400` for invalid syntax or malformed body, `413` for an oversized payload, `415` for wrong content-type, and `422` for schema mismatch if your API contract distinguishes validation errors.

Should the body be logged on validation error?

Not by default. Log request id, route, error class, field names, or a safe summary. The raw body may contain PII, credentials, or large payloads.

When is an idempotency key needed?

For mutations that are dangerous to execute twice: payments, orders, uploads, webhook processing, job creation. The idempotency key should be tied to subject/tenant/route and body hash.

These sources confirm Web Request body methods, the Bun HTTP server model, and the security baseline for uploads and content validation.

Reviewed: 14 May 2026Applies to: Bun 1.3.xApplies to: Bun.serve routesApplies to: Fetch API RequestApplies to: FormDataApplies to: ReadableStreamApplies to: TypeScript validationTested with: Request.json()Tested with: Request.formData()Tested with: Request.arrayBuffer()Tested with: Bun.serve middleware compositionTested with: Zod-style schema validation

PAS7 Studio can help design a validation pipeline for Bun, Hono, or Elysia: JSON schemas, upload security, payload limits, stream ingestion, idempotency keys, stable error contract, and observability.

This is especially useful for SaaS, webhook endpoints, file ingestion, AI/automation flows, and migrations from Express/Fastify where body parsing is often scattered across middleware, handlers, and validators.

You are here05/05

Body parsing and validation in Bun.js: JSON, uploads, streams, and payload limits

Related Articles

ai-assistants

AI Assistant Development Cost in 2026: RAG Chatbots, CRM Integrations, Guardrails, and Support

A practical buyer guide to AI assistant development cost in 2026: prototypes, RAG chatbots, knowledge-base assistants, CRM and website integrations, guardrails, evaluations, monitoring, and support.

blogs

AI for landing page development: where it speeds up launches and where it hurts conversion

A practical research piece on using AI for landing page development: v0, Webflow AI, Builder.io, Framer-like builders, UX generation, copy, SEO, personalization, A/B testing, template risk, accessibility, security and technical debt.

growth

AI SEO / GEO in 2026: Your Next Customers Aren’t Humans — They’re Agents

Search is shifting from clicks to answers. Bots and AI agents crawl, cite, recommend, and increasingly buy. Learn what AI SEO / GEO means, why classic SEO is no longer enough, and how PAS7 Studio helps brands win visibility in the agentic web.

blogs

The most powerful Apple chip yet? M5 Pro and M5 Max are breaking records

A data-backed March 2026 analysis of Apple M5 Pro and M5 Max. We break down why these chips can credibly be called Apple's most powerful pro laptop silicon, how they compare with M4 Pro, M4 Max, M1 Pro, M1 Max, and how they stack up against Intel and AMD laptop rivals.

Professional development for your business

We create modern web solutions and bots for businesses. Learn how we can help you achieve your goals.