For AI agents: the complete documentation index is available at https://dotpress.dev/llms.txt, the full documentation bundle is available at https://dotpress.dev/llms-full.txt, and this page is available as Markdown at https://dotpress.dev/routing/bodies.md.

Bodies and file uploads

Dotpress reads and parses request bodies itself, the same way on every adapter. Validate them with the body and files schemas of the route (see Validation).

Content types

Content-Typereq.body
application/json, *+jsonthe parsed JSON
application/x-www-form-urlencodeda record of strings, arrays for repeated keys
multipart/form-datathe text fields, like a form; the files are in req.files
text/*a string, decoded with the declared charset (UTF-8 by default)
anything elsethe bytes, as a Uint8Array
no bodyundefined

A body that cannot be parsed (invalid JSON, invalid UTF-8, broken multipart) gets 400 MALFORMED_BODY. A compressed body (Content-Encoding: gzip…) or an unknown charset gets 415 UNSUPPORTED_MEDIA_TYPE.

Body size limit

Bodies are limited to 1 MiB by default. A larger body gets 413 PAYLOAD_TOO_LARGE: reading stops as soon as the declared length, or the bytes read, exceed the limit.

Change the limit for the whole app with the bodyLimit option of the adapter, or for one route with its own bodyLimit:

const app = await createApp({ adapter: expressAdapter({ bodyLimit: 256 * 1024 }) })

defineRoute({
  method: 'post',
  path: '/imports',
  bodyLimit: 20 * 1024 * 1024, // 20 MiB, for this route only
  handler,
})

Keep the default limit low, and raise it only on the routes that need it: a large limit everywhere lets any client make the server read large bodies.

File uploads

multipart/form-data bodies are parsed by Dotpress, on every adapter, without Multer or another dependency. Files are Web File objects, validated by the files schema with Zod:

defineRoute<"/profile/avatar", {
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}, false, unknown[]>(route: RouteConfig<"/profile/avatar", {
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/profile/avatar", { files: ZodObject<{ avatar: ZodFile; }, $strip>; body: ZodObject<{ alt: ZodOptional<ZodString>; }, $strip>; }, EmptyObject, false, unknown[], unknown>.path: "/profile/avatar"
path
: '/profile/avatar',
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.bodyLimit?: number | undefined

Maximum request body size in bytes, for instance to accept uploads. Defaults to the adapter limit (1 MiB unless configured otherwise).

bodyLimit
: 3 * 1024 * 1024,
RouteConfig<"/profile/avatar", { files: ZodObject<{ avatar: ZodFile; }, $strip>; body: ZodObject<{ alt: ZodOptional<ZodString>; }, $strip>; }, EmptyObject, false, unknown[], unknown>.schema?: SchemaDefinition<{
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}> | undefined
schema
: (
z: typeof import("zod")
z
) => ({
files: ZodObject<{
    avatar: ZodFile;
}, $strip>
files
:
z: typeof import("zod")
z
.
function object<{
    avatar: ZodFile;
}>(shape?: {
    avatar: ZodFile;
} | undefined, params?: string | {
    error?: string | $ZodErrorMap<NonNullable<$ZodIssueInvalidType<unknown> | $ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): ZodObject<{
    avatar: ZodFile;
}, $strip>
object
({
avatar: ZodFile
avatar
:
z: typeof import("zod")
z
.
function file(params?: string | $ZodFileParams): ZodFile
file
().
ZodFile.max(size: number, params?: string | $ZodCheckMaxSizeParams): ZodFile
max
(2 * 1024 * 1024).
ZodFile.mime(types: MimeTypes | Array<MimeTypes>, params?: string | $ZodCheckMimeTypeParams): ZodFile
mime
(['image/png', 'image/jpeg']),
}),
body: ZodObject<{
    alt: ZodOptional<ZodString>;
}, $strip>
body
:
z: typeof import("zod")
z
.
function object<{
    alt: ZodOptional<ZodString>;
}>(shape?: {
    alt: ZodOptional<ZodString>;
} | undefined, params?: string | {
    error?: string | $ZodErrorMap<NonNullable<$ZodIssueInvalidType<unknown> | $ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): ZodObject<{
    alt: ZodOptional<ZodString>;
}, $strip>
object
({
alt: ZodOptional<ZodString>
alt
:
z: typeof import("zod")
z
.
function string(params?: string | $ZodStringParams): ZodString (+1 overload)
string
().
_ZodString<$ZodStringInternals<string>>.max(maxLength: number, params?: string | $ZodCheckMaxLengthParams): ZodString
max
(200).
ZodType<any, any, $ZodStringInternals<string>>.optional(): ZodOptional<ZodString>
optional
() }),
}),
RouteConfig<"/profile/avatar", { files: ZodObject<{ avatar: ZodFile; }, $strip>; body: ZodObject<{ alt: ZodOptional<ZodString>; }, $strip>; }, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<{
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}, {}, false, unknown>
handler
: async ({
req: RouteRequest<{
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}, {}, false>
req
}) => {
const {
const avatar: File
avatar
} =
req: RouteRequest<{
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}, {}, false>
req
.
DotpressRequest<{}, QueryValues, { alt?: string | undefined; }, { avatar: File; }>.files: {
    avatar: File;
}

Files uploaded with a multipart/form-data body (empty otherwise).

files
const
const bytes: Uint8Array<ArrayBuffer>
bytes
= new
var Uint8Array: Uint8ArrayConstructor
new <ArrayBuffer>(buffer: ArrayBuffer, byteOffset?: number, length?: number) => Uint8Array<ArrayBuffer> (+6 overloads)
Uint8Array
(await
const avatar: File
avatar
.
Blob.arrayBuffer(): Promise<ArrayBuffer>

The arrayBuffer() method of the Blob interface returns a Promise that resolves with the contents of the blob as binary data contained in an ArrayBuffer.

MDN Reference

arrayBuffer
())
const
const url: string
url
= await
const storage: {
    put(key: string, bytes: Uint8Array, type: string): Promise<string>;
}
storage
.
function put(key: string, bytes: Uint8Array, type: string): Promise<string>
put
(`avatars/${
var crypto: Crypto
crypto
.
Crypto.randomUUID(): `${string}-${string}-${string}-${string}-${string}`

The randomUUID() method of the Crypto interface is used to generate a v4 UUID using a cryptographically secure random number generator. Available only in secure contexts.

MDN Reference

randomUUID
()}`,
const bytes: Uint8Array<ArrayBuffer>
bytes
,
const avatar: File
avatar
.
File.type: string

The type read-only property of the Blob interface returns the MIME type of the file.

MDN Reference

type
)
return {
url: string
url
,
alt: string | undefined
alt
:
req: RouteRequest<{
    files: ZodObject<{
        avatar: ZodFile;
    }, $strip>;
    body: ZodObject<{
        alt: ZodOptional<ZodString>;
    }, $strip>;
}, {}, false>
req
.
DotpressRequest<{}, QueryValues, { alt?: string | undefined; }, { avatar: File; }>.body: {
    alt?: string | undefined;
}
body
.
alt?: string | undefined
alt
}
}, })
  • req.files.avatar is a File: name, type, size, arrayBuffer(), stream(), text().
  • The text fields of the form are in req.body, validated by the body schema.
  • An invalid file gets the standard 400 VALIDATION_FAILED, with source: 'files' issues.
  • Files count toward the body limit: raise bodyLimit on upload routes.
  • Files are kept in memory: for very large files, upload them directly to object storage (a presigned URL) rather than through the API.
Check what you accept

The type of a file and its name are sent by the client: they can lie. Limit the size and the types with the schema, generate the storage name yourself, and never serve uploaded files from the domain of your app without a Content-Type you control.

Several files

A field sent several times gives an array. arrayOf() always gives an array, whether the field is sent once or several times:

defineRoute<"/albums/:id/photos", {
    files: ZodObject<{
        photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
    }, $strip>;
}, false, unknown[]>(route: RouteConfig<"/albums/:id/photos", {
    files: ZodObject<{
        photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
    }, $strip>;
}, EmptyObject, false, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/albums/:id/photos", { files: ZodObject<{ photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>; }, $strip>; }, EmptyObject, false, unknown[], unknown>.path: "/albums/:id/photos"
path
: '/albums/:id/photos',
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.bodyLimit?: number | undefined

Maximum request body size in bytes, for instance to accept uploads. Defaults to the adapter limit (1 MiB unless configured otherwise).

bodyLimit
: 50 * 1024 * 1024,
RouteConfig<"/albums/:id/photos", { files: ZodObject<{ photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>; }, $strip>; }, EmptyObject, false, unknown[], unknown>.schema?: SchemaDefinition<{
    files: ZodObject<{
        photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
    }, $strip>;
}> | undefined
schema
: (
z: typeof import("zod")
z
) => ({
files: ZodObject<{
    photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
}, $strip>
files
:
z: typeof import("zod")
z
.
function object<{
    photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
}>(shape?: {
    photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
} | undefined, params?: string | {
    error?: string | $ZodErrorMap<NonNullable<$ZodIssueInvalidType<unknown> | $ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): ZodObject<{
    photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
}, $strip>
object
({
photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>
photos
:
arrayOf<ZodArray<ZodFile>>(schema: ZodArray<ZodFile>): ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>

Validates a field that may be sent once or several times (query string, form fields, files): a single value is wrapped in an array before validation, so the handler always receives an array.

Pass an element schema, or an array schema to add array constraints. Missing values stay undefined: chain .optional() or .default([]).

@exampleschema : (z) => ({ query: z.object({ tag: arrayOf(z.string()).default([]) }), files: z.object({ photos: arrayOf(z.array(z.file().mime(['image/png'])).max(5)) }), })
arrayOf
(
z: typeof import("zod")
z
.
function array<ZodFile>(element: ZodFile, params?: string | $ZodArrayParams): ZodArray<ZodFile>
array
(
z: typeof import("zod")
z
.
function file(params?: string | $ZodFileParams): ZodFile
file
().
ZodFile.mime(types: MimeTypes | Array<MimeTypes>, params?: string | $ZodCheckMimeTypeParams): ZodFile
mime
(['image/jpeg'])).
ZodArray<ZodFile>.max(maxLength: number, params?: string | $ZodCheckMaxLengthParams): ZodArray<ZodFile>
max
(10)),
}), }),
RouteConfig<"/albums/:id/photos", { files: ZodObject<{ photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>; }, $strip>; }, EmptyObject, false, unknown[], unknown>.handler: RouteHandler<{
    files: ZodObject<{
        photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
    }, $strip>;
}, {
    id: string;
}, false, unknown>
handler
: ({
req: RouteRequest<{
    files: ZodObject<{
        photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
    }, $strip>;
}, {
    id: string;
}, false>
req
}) => ({
count: number
count
:
req: RouteRequest<{
    files: ZodObject<{
        photos: ZodPipe<ZodTransform<unknown, unknown>, ZodArray<ZodFile>>;
    }, $strip>;
}, {
    id: string;
}, false>
req
.
DotpressRequest<{ id: string; }, QueryValues, unknown, { photos: File[]; }>.files: {
    photos: File[];
}

Files uploaded with a multipart/form-data body (empty otherwise).

files
.
photos: File[]
photos
.
Array<T>.length: number

Gets or sets the length of the array. This is a number one higher than the highest index in the array.

length
}),
})

Raw bodies for webhooks

Webhook providers sign the exact bytes they send. Declare rawBody: true to receive them in req.rawBody, in addition to the parsed req.body:

defineRoute<"/webhooks/payments", {
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, true, unknown[]>(route: RouteConfig<"/webhooks/payments", {
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, EmptyObject, true, unknown[], unknown>): void

Declares a route on the global router.

defineRoute
({
RouteConfig<P extends string, S extends RouteSchema, TParentParams = EmptyObject, TRawBody extends boolean = false, TProvided extends readonly unknown[] = unknown[], TGroupContext = unknown>.method?: HttpMethod | undefined

Defaults to get.

method
: 'post',
RouteConfig<"/webhooks/payments", { body: ZodObject<{ type: ZodString; id: ZodString; }, $strip>; }, EmptyObject, true, unknown[], unknown>.path: "/webhooks/payments"
path
: '/webhooks/payments',
RouteConfig<"/webhooks/payments", { body: ZodObject<{ type: ZodString; id: ZodString; }, $strip>; }, EmptyObject, true, unknown[], unknown>.rawBody?: true | undefined

Keeps the raw bytes of the request body in req.rawBody, in addition to the parsed body. Use it to verify webhook signatures.

rawBody
: true,
RouteConfig<"/webhooks/payments", { body: ZodObject<{ type: ZodString; id: ZodString; }, $strip>; }, EmptyObject, true, unknown[], unknown>.schema?: SchemaDefinition<{
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}> | undefined
schema
: (
z: typeof import("zod")
z
) => ({
body: ZodObject<{
    type: ZodString;
    id: ZodString;
}, $strip>
body
:
z: typeof import("zod")
z
.
function object<{
    type: ZodString;
    id: ZodString;
}>(shape?: {
    type: ZodString;
    id: ZodString;
} | undefined, params?: string | {
    error?: string | $ZodErrorMap<NonNullable<$ZodIssueInvalidType<unknown> | $ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): ZodObject<{
    type: ZodString;
    id: ZodString;
}, $strip>
object
({
type: ZodString
type
:
z: typeof import("zod")
z
.
function string(params?: string | $ZodStringParams): ZodString (+1 overload)
string
(),
id: ZodString
id
:
z: typeof import("zod")
z
.
function string(params?: string | $ZodStringParams): ZodString (+1 overload)
string
() }) }),
RouteConfig<"/webhooks/payments", { body: ZodObject<{ type: ZodString; id: ZodString; }, $strip>; }, EmptyObject, true, unknown[], unknown>.handler: RouteHandler<{
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, {}, true, unknown>
handler
: async ({
req: RouteRequest<{
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, {}, true>
req
}) => {
if (!(await
const verifySignature: (payload: Uint8Array, signature: string | undefined) => Promise<boolean>
verifySignature
(
req: RouteRequest<{
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, {}, true>
req
.
rawBody: Uint8Array<ArrayBufferLike>

Raw bytes of the body, for instance to verify a webhook signature. Only defined on routes declared with rawBody: true (empty when the request has no body).

rawBody
,
req: RouteRequest<{
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, {}, true>
req
.
DotpressRequest<{}, QueryValues, { type: string; id: string; }, UploadedFiles>.header(name: string): string | undefined

Returns a header value (case-insensitive).

header
('x-signature')))) {
return
function unauthorizedError(message?: string, data?: ErrorData): HttpError
unauthorizedError
('Invalid signature')
} return {
received: string
received
:
req: RouteRequest<{
    body: ZodObject<{
        type: ZodString;
        id: ZodString;
    }, $strip>;
}, {}, true>
req
.
DotpressRequest<{}, QueryValues, { type: string; id: string; }, UploadedFiles>.body: {
    type: string;
    id: string;
}
body
.
id: string
id
}
}, })

req.rawBody is a Uint8Array, typed only on routes declared with rawBody: true. With Express, do not register a native body parser (express.json()) for these routes: the adapter must read the body itself.

Coming from v1

Multer is no longer used: the files option and getFile() are replaced by a files schema and req.files, with Web File objects. The default body limit is 1 MiB (Dotpress 1: 100 KB).