File Uploads
Built-in multipart parsing can read uploaded files into req.files and suits small files with explicit size limits. It does not save files automatically or create a temporary directory. This guide builds a verifiable upload endpoint, then explains route overrides and resource boundaries.
Run a minimal example
Prerequisite: create a Node.js 20+ TypeScript application with Quick Start and npm scripts dev: vext dev, build: vext build, and start: vext start. Add these two files, merging existing settings as needed. This example disables global multipart and enables it only on the upload route.
Create three test files in the example project: a five-byte sample.txt, a 1,025-byte oversized.txt, and a 20 KB large.txt. This command writes those files; choose other names if files with these names already exist:
Run npm run dev; these requests target http://127.0.0.1:3000. Windows PowerShell users can invoke real curl with curl.exe. curl -F generates a boundary automatically; do not add a Content-Type header that omits it:
Stop the development service, run npm run build -- --typecheck and npm start, then repeat these requests. A successful receive proves only parsing and validation. This example does not persist files. If files must be saved, explicitly design the storage destination, naming policy, failure handling, and deletion policy in an application service.
Body and file limits
A multipart request also includes boundaries, form headers, and other overhead. Set maxBodySize above the planned aggregate file size plus required overhead. Increasing maxFileSize does not increase the request read limit. If an adapter imposes another limit, the stricter one applies; multipart settings alone cannot guarantee a large request reaches the application.
Actual semantics of route overrides
- Route
multipart.enabled: trueenables parsing for that route even when global multipart is disabled.falseskips built-in route multipart; when unset, it follows global configuration. - Route-specific parsing middleware is injected only when route
enabled: trueis explicit. Set that switch when route-specificfiles.required, size, count, or MIME checks are needed. - If global parsing already ran, route middleware checks
req.filesagain. A route can tighten limits, but a request already rejected globally never reaches it; a looser route setting cannot recover the request. - To set independent limits for different routes, disable global multipart and enable each route explicitly as in the example. The bodyParser and adapter read boundaries still apply.
- Route
bodyParser.maxBodySizesets a whole-request limit. Compatibility settingoverride.maxBodySizeparticipates only when there is no routebodyParserobject; it is not merged field by field with that object. bodyParser.enabled: falsedisables only that parsing layer, not explicitly registered route multipart. To delegate to custom upload logic, set routemultipart.enabled: falseexplicitly.
Global parsing happens before user authentication middleware. Explicit route multipart is also inserted before user route middleware and the auth Guard. Setting auth on an upload route therefore does not mean authorization precedes the file read. See the Route Definition API for chain order. Applications still need separate identity and object-level permission checks; a parser switch is not authorization.
req.files and form fields
req.files is an optional ParsedFile[]; each entry has fieldname, filename, mimetype, size, and buffer: Buffer. The filename property is filename; the framework does not generate a disk path. It may be undefined when multipart is disabled, not matched, or not parsed, and an empty array when parsing succeeded without files.
The files setting describes fields and checks that required files with those names are present. It is not a field allowlist. Undeclared files may still upload, subject to general size, count, and MIME limits. required: true does not limit the request to one file with that name.
On POST, PUT, and PATCH routes, files also contributes to OpenAPI multipart requestBody generation. Descriptions and required markers inform API consumers, but a files declaration alone does not register route parsing. The example explicitly sets multipart.enabled: true so observed behavior matches the description. See the Route Definition API for complete rules.
Built-in parsing extracts File entries only; it does not place ordinary multipart text fields into req.body. To validate text fields too, choose custom parsing that explicitly supports that contract. Do not assume req.valid("body") can read those text fields.
allowedMimeTypes compares the parsed File MIME value; it does not inspect content. An empty File.type is treated as application/octet-stream. An unset list imposes no restriction; an empty array accepts no MIME values. Applications must inspect content when a trustworthy format is needed, and must not use an untrusted filename directly as a storage path.
Memory, adapters, and custom uploads
The built-in flow reads a bounded raw Buffer and parses the form using Web APIs, keeping file content in ParsedFile.buffer. Adapters share this parser but have their own request-read boundaries. This is not streaming persistence. Concurrent uploads can hold several copies of request and file data in memory, so a per-file limit is not a process memory budget.
There is no built-in tmpDir, disk-retention TTL, or scheduled cleanup task. Request completion also does not explicitly zero every Buffer immediately; the runtime reclaims unreferenced memory. The application owns files saved to disk or object storage, failed-write residue, and connection lifecycles.
For large files, stream-as-you-read, resumable uploads, or custom form semantics, choose a custom upload solution that fits the actual adapter and storage capabilities, and first disable built-in parsing that would consume the request body. _getRawBodyBuffer() still reads all content into memory and is not a streaming substitute. See Plugins for integration and Security and Resources for ownership.
Errors and rechecks
Built-in parser failures return direct JSON with code, message, and requestId, without the successful response's data wrapper.
See the Configuration API, Request Context API, and Route Definition API for all field definitions.