@nextrush/body-parserBody Parser
Parse JSON, URL-encoded, text, and raw request bodies with size limits and security protections.
Parse incoming request bodies before your handlers process them.
Source & internals
HTTP request bodies arrive as raw byte streams. This middleware reads, validates, and parses them into usable JavaScript values — handling JSON, URL-encoded forms, plain text, and raw binary data. Built-in protections guard against oversized payloads, prototype pollution, and malformed input.
Default Behavior
With default options, bodyParser() parses JSON and URL-encoded request bodies. Text and raw binary parsing stay off until you enable them.
Bodyless HTTP methods (GET, HEAD, DELETE, OPTIONS) are skipped automatically. If the body has already been parsed by another middleware, parsing is skipped.
Installation
$ pnpm add @nextrush/body-parser
Minimal Usage
import { createApp } from '@nextrush/core';
import { bodyParser } from '@nextrush/body-parser';
const app = createApp();
// Parses JSON and URL-encoded bodies by default
app.use(bodyParser());
app.use(async (ctx) => {
ctx.json({ received: ctx.body });
});Individual Parsers
Use individual parsers when you need to handle specific content types only.
import { json } from '@nextrush/body-parser';
app.use(json());
// Parses: application/jsonConfiguration Options
bodyParser(options?)
Combined parser that routes to the appropriate parser based on Content-Type.
function bodyParser(options?: BodyParserOptions): Middleware;Options
| Property | Type | Description |
|---|---|---|
json | JsonOptions | false= {} | JSON parser options, or false to disable |
urlencoded | UrlEncodedOptions | false= {} | URL-encoded parser options, or false to disable |
text | TextOptions | false= undefined | Text parser options — disabled by default |
raw | RawOptions | false= undefined | Raw parser options — disabled by default |
lazy | boolean= false | Defer every parser until `await ctx.readBody()` |
JSON and URL-encoded parsing are enabled by default. To enable text or raw parsing, provide an options object (even {} activates them). To disable a parser, set it to false.
app.use(
bodyParser({
json: { limit: '10mb', strict: true },
urlencoded: { extended: true, depth: 5 },
text: {}, // Enable text parsing with defaults
raw: false, // Explicitly disable raw parsing
})
);Multipart requests (multipart/form-data) throw an UNSUPPORTED_CONTENT_TYPE error with a hint to use a dedicated multipart parser.
json(options?)
function json(options?: JsonOptions): Middleware;JSON Options
| Property | Type | Description |
|---|---|---|
limit | number | string= '1mb' (1,048,576 bytes) | Maximum body size in bytes or as a string with unit |
strict | boolean= true | Accept only objects and arrays — rejects primitives |
type | string | string[]= ['application/json'] | Content-Type(s) to match |
rawBody | boolean= false | Store raw Buffer on ctx.rawBody |
reviver? | JsonReviver | Custom reviver passed to JSON.parse() |
maxDepth? | number | Maximum JSON nesting depth — rejects deeper payloads after parsing |
verify? | VerifyCallback | Callback invoked with raw buffer before parsing — throw to reject |
urlencoded(options?)
function urlencoded(options?: UrlEncodedOptions): Middleware;URL-Encoded Options
| Property | Type | Description |
|---|---|---|
limit | number | string= '100kb' (102,400 bytes) | Maximum body size in bytes or as a string with unit |
extended | boolean= true | Enable nested object parsing (e.g. user[name]=John) |
parameterLimit | number= 1000 | Maximum number of parameters accepted |
depth | number= 20 | Maximum nesting depth for extended parsing |
type | string | string[]= ['application/x-www-form-urlencoded'] | Content-Type(s) to match |
rawBody | boolean= false | Store raw Buffer on ctx.rawBody |
verify? | VerifyCallback | Callback invoked with raw buffer before parsing — throw to reject |
text(options?)
function text(options?: TextOptions): Middleware;Text Options
| Property | Type | Description |
|---|---|---|
limit | number | string= '100kb' (102,400 bytes) | Maximum body size in bytes or as a string with unit |
defaultCharset | SupportedCharset= 'utf-8' | Fallback charset when not specified in Content-Type |
type | string | string[]= ['text/plain'] | Content-Type(s) to match |
rawBody | boolean= false | Store raw Buffer on ctx.rawBody |
verify? | VerifyCallback | Callback invoked with raw buffer before parsing — throw to reject |
raw(options?)
function raw(options?: RawOptions): Middleware;Raw Options
| Property | Type | Description |
|---|---|---|
limit | number | string= '100kb' (102,400 bytes) | Maximum body size in bytes or as a string with unit |
type | string | string[]= ['application/octet-stream'] | Content-Type(s) to match |
verify? | VerifyCallback | Callback invoked with raw buffer before parsing — throw to reject |
Lazy Parsing
By default the parser reads, verifies, and parses the body before next() runs. With
lazy: true it stops after detection and defers the rest to await ctx.readBody():
app.use(bodyParser({ lazy: true }));
app.post('/webhooks/github', async (ctx) => {
// A request that never reaches this line costs no body work at all.
const payload = await ctx.readBody<{ action: string }>();
return ctx.json({ received: payload.action });
});Detection still runs eagerly — method, media type, and an already-parsed body decide who owns the
request — so multipart/form-data and onUnsupportedContentType: 'reject' keep answering 415
before your handler. Bytes, the size limit, verify, decoding, parsing, and the JSON depth guard all
run inside readBody().
readBody() exists in both modes, is idempotent, shares one read between concurrent callers, and
caches a failure for the rest of the request.
In lazy mode ctx.body throws BODY_NOT_READ until readBody() succeeds, so a missing await
fails loudly instead of looking like an empty body. That applies to requests the parser claimed —
a bodyless method or an unmatched Content-Type under the default policy still leaves it
undefined.
What changes when you opt in:
| Eager (default) | lazy: true |
|---|---|
ctx.body holds the parsed value before next() | On a claimed request, ctx.body throws BODY_NOT_READ until readBody() succeeds |
A malformed body rejects before next() | It rejects at the readBody() call site, so your handler can catch it |
An oversized body answers 413 before next() | 413 only if the body is read — the adapter's transport cap still applies |
ctx.rawBody is populated before next() | Populated by readBody(), at the same moment as ctx.body |
Use eager parsing when most requests need the body, when a malformed payload must be rejected before
any handler runs, or through @nextrush/express-bridge (whose req.body is a synchronous getter).
Measured on Bun 1.4.4, a 1 KB JSON POST whose handler never reads the body drops from 9.39 µs to
6.45 µs per request (−31 %); a request that does read it pays roughly 0.4–0.5 µs more than eager
parsing.
Integration Example
An API server with error handling, JSON parsing, and webhook signature verification:
import { createApp } from '@nextrush/core';
import { json, BodyParserError } from '@nextrush/body-parser';
import { createHmac } from 'node:crypto';
const app = createApp();
// Error handler (register before body parser)
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
if (error instanceof BodyParserError) {
ctx.status = error.status;
ctx.json({ error: error.message, code: error.code });
return;
}
throw error;
}
});
// Parse JSON with raw body access for webhook verification
app.use(json({ limit: '1mb', rawBody: true }));
app.post('/webhook', async (ctx) => {
const signature = ctx.headers['x-signature'] as string;
const rawBody = ctx.rawBody as Buffer;
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET!).update(rawBody).digest('hex');
if (signature !== expected) {
ctx.status = 401;
ctx.json({ error: 'Invalid signature' });
return;
}
ctx.json({ ok: true });
});Error Handling
All parsers throw BodyParserError on failure. Each error includes status (HTTP status code), code (machine-readable string), and expose (whether the message is safe for clients).
import { BodyParserError } from '@nextrush/body-parser';
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
if (error instanceof BodyParserError) {
ctx.status = error.status;
ctx.json({ error: error.message, code: error.code });
return;
}
throw error;
}
});Error Codes
| Code | Status | Cause |
|---|---|---|
ENTITY_TOO_LARGE | 413 | Body exceeds configured size limit |
INVALID_JSON | 400 | JSON syntax error |
STRICT_MODE_VIOLATION | 400 | JSON value is a primitive in strict mode |
JSON_DEPTH_EXCEEDED | 400 | JSON nesting depth exceeds maxDepth |
INVALID_URLENCODED | 400 | Malformed URL-encoded data |
TOO_MANY_PARAMETERS | 413 | URL-encoded parameter count exceeds parameterLimit |
DEPTH_EXCEEDED | 400 | URL-encoded nesting exceeds depth limit |
INVALID_PARAMETER | 400 | Prototype pollution attempt (__proto__, constructor, prototype) |
UNSUPPORTED_CHARSET | 415 | Charset in Content-Type is not supported |
UNSUPPORTED_CONTENT_TYPE | 415 | Content type not handled (e.g. multipart/form-data) |
BODY_READ_ERROR | 400 | Error reading request stream |
REQUEST_CLOSED | 400 | Connection closed before body was received |
REQUEST_ABORTED | 400 | Client aborted the request |
BODY_NOT_READ | 500 | ctx.body read under lazy: true before await ctx.readBody() |
Common Mistakes
bodyParser() does not parse text or raw bodies by default. Pass text: {} or raw: {} in
the options to enable them.
- Using
bodyParser()and expecting text/raw parsing — these are disabled by default. Enable them explicitly:bodyParser({ text: {}, raw: {} }). - Setting the wrong size limit units — limits accept
'b','kb','mb','gb'. A bare number is treated as bytes. - Expecting multipart form uploads to work — this parser does not handle
multipart/form-data. Use a dedicated multipart parser. - Forgetting error handling — without an error-catching middleware upstream,
BodyParserErrorwill crash your app.
Troubleshooting
ctx.body is undefined after parsing:
- Verify the request has a matching
Content-Typeheader. - Check the HTTP method —
GET,HEAD,DELETE, andOPTIONSrequests are skipped. - Confirm no earlier middleware has already set
ctx.body.
413 errors on valid payloads:
- Increase the
limitoption. The JSON default is1mb; URL-encoded, text, and raw default to100kb.
UNSUPPORTED_CONTENT_TYPE error:
- The combined parser throws this for
multipart/form-datarequests. Use a separate multipart parser.
BODY_NOT_READ error:
lazy: trueis active and something readctx.bodybefore awaiting the read. Callconst body = await ctx.readBody()first, or droplazy: true.
Related
- Middleware Overview — All middleware packages
- @nextrush/cors — CORS handling
- @nextrush/rate-limit — Rate limiting