Skip to Content

@TypedFormData

@nestia/core
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

src/controllers/ArticlesController.ts
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 typeWire payloadYou receive
Fileone file fieldFile
File[]repeated file fieldsFile[]
File?optional fileFile or undefined
stringplain text field"..."
numberplain text fieldcoerced to number (or 400)
booleanplain text fieldcoerced to boolean (or 400)
string[]repeated plain fieldsstring[]
Tagged typesas above + validationvalidated 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:

RejectionStatus
A file over limits.fileSize413 File too large
Another limit: file count, field count, part count, field name or value length400 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 field400 Unexpected field - <field>
A malformed body: no boundary, a truncated form, a malformed part header400 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 @TypedFormData routes appear in the generated client.
Last updated on