#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-Type | req.body |
|---|---|
application/json, *+json | the parsed JSON |
application/x-www-form-urlencoded | a record of strings, arrays for repeated keys |
multipart/form-data | the text fields, like a form; the files are in req.files |
text/* | a string, decoded with the declared charset (UTF-8 by default) |
| anything else | the bytes, as a Uint8Array |
| no body | undefined |
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 | undefinedDefaults 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 | undefinedMaximum 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.
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.
randomUUID ()}`, const bytes: Uint8Array<ArrayBuffer> bytes , const avatar: File avatar .File.type: stringThe type read-only property of the Blob interface returns the MIME type of the file.
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.avataris aFile:name,type,size,arrayBuffer(),stream(),text().- The text fields of the form are in
req.body, validated by thebodyschema. - An invalid file gets the standard
400 VALIDATION_FAILED, withsource: 'files'issues. - Files count toward the body limit: raise
bodyLimiton 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.
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 | undefinedDefaults 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 | undefinedMaximum 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: numberGets 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 | undefinedDefaults 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 | undefinedKeeps 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 | undefinedReturns 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.
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).