@TypedFormData
export namespace TypedFormData {
export function Body<Multer extends IMulterBase>(
factory: () => Multer | Promise<Multer>,
): ParameterDecorator;
/** Base type of the `multer` or `fastify-multer` instance. */
export interface IMulterBase {
single(fieldName: string): any;
array(fieldName: string, maxCount?: number): any;
fields(fields: readonly object[]): any;
any(): any;
none(): any;
}
}Parameter decorator for multipart/form-data request bodies — the content type browsers send when an HTML form contains a <input type="file">. Use it for any endpoint that accepts file uploads alongside other fields.
Replaces the NestJS combination of @UploadedFile() / @UploadedFiles() / FileFieldsInterceptor / @ApiConsumes("multipart/form-data") / @ApiBody({...}) with a single decorator that reads everything from the parameter’s TypeScript type.
Only @TypedFormData.Body() is supported by @nestia/sdk’s generators. Vanilla @UploadedFile() routes don’t appear in the generated SDK or Swagger document.
Basic usage
import { TypedFormData, TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import Multer from "multer";
interface IArticleCreate {
title: string;
body: string | null;
thumbnail?: File;
attachments: File[];
tags: string[];
}
@Controller("articles")
export class ArticlesController {
@TypedRoute.Post()
public async create(
@TypedFormData.Body(() => Multer()) input: IArticleCreate,
): Promise<{ id: string }> {
// input.thumbnail is a Blob | undefined
// input.attachments is Blob[]
// input.title / body / tags are validated strings / arrays
return { id: "..." };
}
}Multipart fields with a binary file (browser sends Content-Disposition: form-data; name="…"; filename="…") become File (a browser-compatible Blob subclass). Plain fields become their declared TypeScript type with the same coercion rules as @TypedQuery.
What gets parsed
| Declared type | Wire payload | You receive |
|---|---|---|
File | one file field | File |
File[] | repeated file fields | File[] |
File? | optional file | File or undefined |
string | plain text field | "..." |
number | plain text field | coerced to number (or 400) |
boolean | plain text field | coerced to boolean (or 400) |
string[] | repeated plain fields | string[] |
| Tagged types | as above + validation | validated value (or 400) |
Constraints via tags.* work the same as on JSON bodies — string & tags.MinLength<3>, number & tags.Maximum<100>, etc.
File handling
The File you receive is a Blob. To read it as a buffer:
import { TypedFormData, TypedRoute } from "@nestia/core";
@TypedRoute.Post()
public async create(
@TypedFormData.Body(() => Multer()) input: { upload: File },
): Promise<void> {
const buf = Buffer.from(await input.upload.arrayBuffer());
// upload to S3, save to disk, etc.
}input.upload.name is the filename, input.upload.type the MIME type, input.upload.size the byte length.
Multer configuration
The factory argument is a function that returns a configured Multer instance. You control storage, file-size limits, and filtering through Multer’s standard options:
import Multer from "multer";
@TypedRoute.Post()
public async upload(
@TypedFormData.Body(() => Multer({
storage: Multer.memoryStorage(),
limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
})) input: IUpload,
): Promise<void> { ... }On Fastify, pass a fastify-multer instance instead:
import FastifyMulter from "fastify-multer";
@TypedRoute.Post()
public async upload(
@TypedFormData.Body(() => FastifyMulter()) input: IUpload,
): Promise<void> { ... }Fastify also needs a multipart/form-data content type parser that leaves the request stream to fastify-multer; without one it answers every multipart request with 415. Register it when composing the application. fastify-multer’s own contentParser plugin declares the content type multipart without a subtype, which Fastify 5 (NestJS 11) refuses to register.
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
app
.getHttpAdapter()
.getInstance()
.addContentTypeParser("multipart/form-data", (_req, _payload, done) =>
done(null),
);Rejected uploads
A request the Multer configuration rejects answers the client error NestJS’s own FileInterceptor gives it:
| Rejection | Status |
|---|---|
A file over limits.fileSize | 413 File too large |
| Another limit: file count, field count, part count, field name or value length | 400 with Multer’s message and the field |
| A file on a field the type does not declare, or a second file on a single-file field | 400 Unexpected field - <field> |
| A malformed body: no boundary, a truncated form, a malformed part header | 400 Multipart: <reason> |
An HttpException your fileFilter passes to its callback is kept as it is.
Stored files
With disk storage (dest or diskStorage), each file is read into the File your handler receives and then removed from disk, whether the request succeeds or fails. The handler never sees a path. To keep uploads, write the File where you want it from the handler.
Comparison with vanilla NestJS
// Vanilla NestJS — separate decorators for files vs fields, separate Swagger schema
@Post()
@ApiConsumes("multipart/form-data")
@UseInterceptors(
FileFieldsInterceptor([
{ name: "thumbnail", maxCount: 1 },
{ name: "attachments" },
]),
)
@ApiBody({
schema: {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string", nullable: true },
thumbnail: { type: "string", format: "binary" },
attachments: { type: "array", items: { type: "string", format: "binary" } },
},
},
})
async create(
@Body() body: any,
@UploadedFiles() files: { thumbnail?: Express.Multer.File[]; attachments?: Express.Multer.File[] },
) { ... }Versus the Nestia version above — one decorator, no Swagger schema, no UseInterceptors, no array-of-array unwrapping. The TypeScript type is the contract.
See also
- TypedBody — JSON bodies (when no files are involved).
- TypedQuery.Body — form-urlencoded bodies (text fields only).
- SDK — how
@TypedFormDataroutes appear in the generated client.