đź“– Guide Documents
Core Library
TypedFormData

Outline

@nestia/core
export namespace TypedFormData {
  export function Body<Multer extends IMulterBase>(
    factory: () => Multer | Promise<Multer>
  ): ParameterDecorator;
  export type IMulterBase = ExpressMulter.Multer | FastifyMulter.Multer;
}

Request body decorator of multipart/form-data.

@TypedFormData.Body() is a request body decorator function, for the multipart/form-data content type. It is useful for file uploading with additional data, and automatically casts property type following its DTO definition, performing the type validation.

As you can see from the below code, @TypedFormData.Body() function is much easier and type safer than @UploadFile() of NestJS. Also, if you're considering the SDK library generation, only @TypedFormData.Body() is supported. Therefore, I recommend you to utilize @TypedFormData.Body() instead of the @UploadFile() function.

Of course, as every features of nestia does, you don't need to define any extra schema definition for the Swagger Documents generation. @nestia/sdk and @TypedFormData.Body() will do everything just by analyzing your TypeScript types and codes.

nestia/MultipartController.ts
import { TypedFormData, TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import Multer from "multer";
 
@Controller("bbs/articles")
export class BbsArticlesController {
  @TypedRoute.Post()
  public async create(
    @TypedFormData.Body(() => Multer()) input: IBbsArticleCreate,
  ): Promise<void> {
    input;
  }
}
 
export interface IBbsArticleCreate {
  title: string;
  body: string | null;
  thumbnail?: File | undefined;
  files: File[];
  tags: string[];
}

How to use

BbsArticlesController.ts
import { TypedFormData, TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import Multer from "multer";
 
import { IBbsArticleCreate } from "./IBbsArticleCreate";
 
@Controller("bbs/articles")
export class BbsArticlesController {
  @TypedRoute.Post()
  public async create(
    @TypedFormData.Body(() => Multer()) input: IBbsArticleCreate,
  ): Promise<void> {
    input;
  }
}

Just call @TypedFormData.Body() function on the request body parameter, that's all.

Nestia will analyze your type (IBbsArticleCreate), and writes optimal code for the target type, in the compilation level. If you click the "Compiled JavaScript File" tab of above, you can see the optimal transformation and validation code.

Such optimization is called AOT (Ahead of Time) compilation, and it is the secret of @TypedFormData.Body.

By the way, if you're using fastify, you have to setup fastify-multer and configure like below when composing the NestJS application. If you don't do it, @TypedFormData.Body() will not work properly, and throw 500 internal server error when Blob or File type being utilized.

// main.ts
import { NestFactory } from "@nestjs/core";
import { 
  FastifyAdapter, 
  NestFastifyApplication 
} from "@nestjs/platform-fastify";
import FastifyMulter from "fastify-multer";
 
export async function main() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );
  app.register(FastifyMulter.contentParser as any);
  await app.listen(3000);
}
 
// BbsArticlesController.ts
import { TypedFormData, TypedRoute } from "@nestia/core";
import { Controller } from "@nestjs/common";
import FastifyMulter from "fastify-multer";
 
import { IBbsArticleCreate } from "./IBbsArticleCreate";
 
@Controller("bbs/articles")
export class BbsArticlesController {
  @TypedRoute.Post()
  public async create(
    @TypedFormData.Body(() => FastifyMulter()) input: IBbsArticleCreate,
  ): Promise<void> {
    input;
  }
}

Special Tags

You can enhance validation logic, of @TypedFormData.Body(), through comment tags.

You know what? @TypedFormData.Body() utilizes typia.assert<T>() (opens in a new tab) function for form data validation, and the typia.assert<T>() (opens in a new tab) function supports additional type checking logics through comment tags. For reference, "Type Tag" means a intersection type with atomic type and special tag type of typia like number & tags.Type<"uint32">, and "Comment Tag" means a comment starting from @ symbol following @${name} ${value} format.

With those type and comment tags, you can add additional validation logics. If you want to add a custom validation logic, you also can do it. Read below Guide Documents of typia (opens in a new tab), and see the example code. You may understand how to utilize such type and comment tags, in a few minutes.

examples/src/is-special-tags.ts
import typia, { tags } from "typia";
 
export const checkSpecialTag = typia.createIs<SpecialTag>();
 
interface SpecialTag {
  int32: number & tags.Type<"int32">;
  range?: number & tags.ExclusiveMinimum<19> & tags.Maximum<100>;
  minLength: string & tags.MinLength<3>;
  pattern: string & tags.Pattern<"^[a-z]+$">;
  date: null | (string & tags.Format<"date">);
  ip: string & (tags.Format<"ipv4"> | tags.Format<"ipv6">);
  uuids: Array<string & tags.Format<"uuid">> &
    tags.MinItems<3> &
    tags.MaxItems<100>;
}

Restriction

When using @TypedFormData.Body(), you've to follow such restrction.

At first, type of @TypedFormData.Body() must be a pure object type. It does not allow union type. Also, nullable and undefindable types are not allowed, either. Note that, query parameter type must be a sole object type without any extra definition.

At next, type of properties must be atomic, Blob, File or array of them. In the atomic type case, the atomic type allows both nullable and undefindable types. However, mixed union atomic type like string | number or "1" | "2" | 3 are not allowed. Also, the array type does not allow both nullable and undefindable types, either.

  • boolean
  • number
  • bigint
  • string
  • Blob
  • File
SomeFormDataDto.ts
export interface SomeFormDataDto {
  //----
  // ATOMIC OR FILE TYPES
  //----
  // ALLOWED
  boolean: boolean;
  number: number;
  string: string;
  bigint: bigint;
  optional_number?: number;
  nullable_string: string | null;
  literal_union: "A" | "B" | "C" | "D";
  blob: Blob;
  file: File;
 
  // NOT ALLOWED
  mixed_union: string | number | boolean;
  mixed_literal: "A" | "B" | 3;
 
  //----
  // ARRAY TYPES
  //----
  // ALLOWED
  nullable_element_array: (string | null)[];
  string_array: string[];
  number_array: number[];
  literal_union_array: ("A" | "B" | "C")[];
  literal_tuple: ["A", "B", "C"];
  blobs: Blob[];
  files: File[];
 
  // NOT ALLOWED
  optional_element_array: (string | undefined)[];
  optional_array: string[] | undefined;
  nullable_array: string[] | null;
  union_atomic_array: (string | number)[];
  mixed_literal_array: ("A", "B", 3)[];
  mixed_tuple: ["A", "B", 3];
}