Skip to content

File uploads

A request with Content-Type: multipart/form-data makes @body() resolve to { fields, files } instead of a plain object.

import { Route, Post, body } from '@green-tea/core';
import type { MultipartBody } from '@green-tea/core';
@Route('/profile')
class ProfileController {
@Post('/avatar')
upload(@body() form: MultipartBody) {
const name = form.fields.name; // string | string[]
const avatar = form.files.avatar; // UploadedFile | UploadedFile[]
return { name, size: Array.isArray(avatar) ? undefined : avatar?.size };
}
}

UploadedFile is { filename, contentType, data: Buffer, size } — the whole file is buffered in memory, no temp files.

By default, a repeated field name keeps the last value ('last', matching urlencoded). Set bodyDuplicates: 'array' on createApp to accumulate repeats into a string[] instead — this applies to both urlencoded and multipart text fields:

const app = createApp({ modules: [ApiModule], bodyDuplicates: 'array' });

Files under a repeated field name always become an array (UploadedFile[]), regardless of bodyDuplicates.

@Post('/upload', { duplicates: 'array' })
upload(@body() form: MultipartBody) { /* ... */ }

Precedence: route duplicates → app bodyDuplicates'last'.

Uploaded files are held in memory, so they’re bounded by the same maxBodyBytes limit as any other request body (over the limit → 413). maxParts (default 1000) caps the number of multipart parts per request:

const app = createApp({ modules: [ApiModule], limits: { maxParts: 500 } });

Both limits can be overridden per route — an upload endpoint can allow a larger body than the rest of the API:

@Post('/avatar', { maxBodyBytes: 5_000_000, maxParts: 20 })
uploadAvatar(@body() form: MultipartBody) { /* ... */ }

A route’s maxBodyBytes / maxParts fall back to the server-wide limits when unset. A malformed multipart body (bad boundary, missing headers, truncated part) → 400.