NestJS (DTOの基本をマスターしよう!) #TypeScript - Qiita

はじめに

今回の記事では、 「NestJSのDTOの実装方法」 をまとめました!前半では知識の整理や理解(インプット)、後半では要件やテーブルモデルから実際にDTOを実装する(アウトプット)のような形式になっています!

私も実務の中でDTOの実装は行っているため、既存の実装を参考にしながら実装することは可能 です。しかし、「世界一流エンジニアの思考法」という書籍で述べられていましたが、実装スピードを高めるには、「調べれば実装できること」 を減らし、「調べなくても実装できること」 を増やす必要があると考えています。

記事の最後に練習問題を用意しているので、私と一緒に 「既存の実装を確認せず、爆速で実装できるレベル」 を目指しましょう!

前提

この記事は、TypeScriptやNestJSの実装経験 があり、DTOについてもっと理解したい!という人向けに記述しています。

経験のある人や普段の実務でNestJSのDTOを実装している人は、「問題を解く」→「該当箇所のインプットを確認する」といったほうが効率が良さそうです。

各内容へのリンク

インプット編

アウトプット編

インプット編

1. ドキュメント化

@ApiProperty({
    description: '発送日', 
    example: '2025-01-01',
    type: String, ⇦ ここでの型定義は大文字始まり「String」「Number」などにすること
})

【補足(1)】typeに「string」や「number」を指定してはダメなの?

@ApiProperty() の type は JavaScript のコンストラクタ関数(型のクラス) を指定する仕様 になっており、「string」や「number」は型エラーが発生する可能性があるそうです。

2. 型定義 (下記で定義した以外の値を受け取るとエラーになる)

@IsString()
⇨ 文字列であることを保証

@IsBoolean()
⇨ 真偽値であることを保証

@IsEnum(ENUM_VALUE)
⇨ 指定したEnumのいずれかの値であることを保証

@IsDate()
⇨ Data型であることを保証

@IsNumber()
⇨ 数値であることを保証

@IsObject()
⇨ オブジェクト型であることを保証

@IsArray()
⇨ 配列であることを保証

3. バリデーション

@IsNotEmpty()
⇨ 「null」「undefined」「空の値」を許可しない

@IsOptional()
⇨ リクエストがなくても、エラーにならない (注意:リクエストを受け取った場合は、型定義にバリデーションに従う)

@ValidateNested({ each: true })
⇨ リクエストでオブジェクトの配列を受け取った際、全てのデータにvalidationを適用する

@ValidateNested()
⇨ リクエストでオブジェクトを受け取った際、validationを適用する

@ValidateIf((_, value) => value !== null)
⇨ 「Null」を許容

【補足】@IsOptionalとの違いは?

1 @IsOptionalは「undifined」の場合に処理をスキップ

2 @ValidateIf((_, value) => value !== null)は「null」の場合に処理をスキップ

4. 型変換、値のカスタム

4-1 @Type(() => XXXX)

@Type(() => Number)
⇨ 受け取った値を数値に変換

@Type(() => Date)
⇨ 受け取った値をDate型に変換

【補足】

1 どんな時に型変換が必要なの?
  • パターン1 (GETメソッドの検索パラメータでDTOを指定)

⇨ リクエストパラメータ(クエリ)で受け取った場合、値は 基本的に「文字列」で受け取る ため、数値に変換する必要がある

  • パターン2 (日付を文字列で受け取る場合を考慮)

ISO 8601 形式(2025-04-01T12:30:00Z)の文字列を受け取る場合、「@Type(() => Date)」を使って「Date型」に変換する必要がある

4-2 @Transform(({ value }) => xxxx)

@Transform(({ value }) => startOfDay(value), { toClassOnly: true })
⇨ 受け取った値を「日付の開始時刻(00:00:00)」に変換する。 (toClassOnly: true)はリクエストからクラスに変換する際にのみ適用され、レスポンス時には適用されない!

@Transform(({ value }) => typeof value === ‘string' && value.toLowerCase() === 'true')
⇨ 文字列で受け取った真偽値(ex: “true“)を真偽値(true)に変換する

@Transform(({ value }) => (value == null ? null : endOfDay(value)))
⇨ nullを許容するパターン。下記の実装でnullの場合にバリデーションをスキップする

アウトプット編

下記に「実装についての要件」と「どんなリクエストが必要か」についての記述があります。これらを確認して、実際にDTOを実装しましょう。

例題(1) 【SearchUserDto】

要件
  • ユーザーの検索DTOを実装したい
  • @ApiPropertyの実装は任意です

必要なリクエスト内容

解答例
export class SearchUserDto {
  @ApiPropertyOptional({
    description: 'ユーザー名',
    type: String,
  })
  @IsString()
  @IsOptional()
  name?: string;

  @ApiPropertyOptional({
    description: 'ユーザーコード',
    type: String,
  })
  @IsString()
  @IsOptional()
  code?: string;

  @ApiPropertyOptional({
    description: '年齢',
    type: Number,
  })
  @IsNumber()
  @Type(() => Number)
  @IsOptional()
  age?: number;
}
実装のポイント

  • @ApiPropertyOptional」のtypeは「JavaScript のコンストラクタ関数」で指定しよう!
  • 検索用のDTOなので、「@IsOptional()」をつけよう!
  • 「name?: string;」のようにオプショナルにしよう!

例題(2) 【CreateUserDto】

要件
  • ユーザーの新規作成DTOを実装したい
  • @ApiPropertyの実装は任意です

必要なリクエスト内容

  • ユーザーコード(必須) (code: string)

  • ユーザー名(必須) (name: string)

  • ニックネーム(空文字を許容) (nickName: string)

  • プロフィール (profile: string)

解答例
export class CreateUserDto {
  @ApiProperty({
    description: 'ユーザーコード',
    type: String,
  })
  @IsString()
  @IsNotEmpty()
  code: string;

  @ApiProperty({
    description: 'ユーザー名',
    type: String,
  })
  @IsString()
  @IsNotEmpty()
  name: string;
  
  @ApiProperty({
    description: 'ニックネーム',
    type: String,
  })
  @IsString()
  nickName: string;
  
  @ApiProperty({
    description: 'プロフィール',
    type: String,
  })
  @IsString()
  profile: string;
}
実装のポイント

  • 必須項目には、@IsNotEmpty()をつけよう!
  • 住所(location)と備考(note)は未入力時に空文字を送るので、「@IsNotEmpty()」はつけません!

練習問題(1) 【SearchPostDto】

要件
  • 投稿の検索DTOを実装したい
  • @ApiPropertyの実装は必須とします

必要なリクエスト内容

  • 投稿名 (name: string)
  • 投稿番号 (code: string)
  • ユーザーID (userId: number)
  • コメントID (commentId: number)
  • お気に入りID (favoriteId: number)
解答例
export class SearchPostDto {
  @ApiProperty({
    description: '投稿名',
    type: String,
  })
  @IsString()
  @Optional()
  name?: string;

  @ApiProperty({
    description: '投稿番号',
    type: String,
  })
  @IsString()
  @IsOptional()
  code?: string;
  
  @ApiProperty({
    description: 'ユーザーID',
    type: Number,
  })
  @IsNumber()
  @IsOptional()
  userId?: number;
  
  @ApiProperty({
    description: 'コメントID',
    type: Numer,
  })
  @IsNumber()
  @IsOptional()
  commentId?: number;

  @ApiProperty({
    description: 'お気に入りID',
    type: Number,
  })
  @IsNumber()
  @IsOptional()
  favoriteId?: number
}

練習問題(2) 【CreateProfileDto】

要件
  • プロフィールの新規作成DTOを実装したい
  • @ApiPropertyの実装は必須とします

必要なリクエスト内容

  • 身長 (height: string)
  • 体型 (bodyType: BodyType)
  • 職業 (occupation: string | undefined)
  • 学歴 (education: EducationType | null)
  • 出身地 (hometown: string)
  • 居住地 (location: string | null)
  • 趣味 (hobbyIds: number[])
  • 誕生日 (birthDay: Date)
  • 休日の過ごし方 (weekendActivities: WeekendActivitiesDto)

実装の上で注意すること

  • 「BodyType」と「EducationType」はEnumで実装されています
  • 誕生日については、「リクエスト受け取り時は文字列で受け取ること」「日付の開始時刻に変換すること」
  • 「WeekendActivitiesDto」はオブジェクトです
解答例
export class SearchPostDto {
  @ApiProperty({
    description: '身長',
    type: String,
  })
  @IsString()
  @IsNotEmpty() ⇦ これがないと「空文字」を許容してしまいます!
  height: string;

  @ApiProperty({
    description: '体型',
    type: BodyType,
  })
  @IsEnum(BodyType)
  bodyType: BodyType;
  
  @ApiProperty({
    description: '職業',
    type: Number,
  })
  @IsNumber()
  @IsOptional()
  occupation?: string;
  
  @ApiProperty({
    description: '学歴',
    type: EducationType,
  })
  @IsEnum(EducationType)
  @ValidateIf((_, value) => value !== null)
  education: EducationType | null;

  @ApiProperty({
    description: '出身地',
    type: String,
  })
  @IsString()
  @IsNotEmpty()
  hometown: string
  
  @ApiProperty({
    description: '居住地',
    type: String,
  })
  @IsString()
  @ValidateIf((_, value) => value !== null)
  location: string | null
  
  @ApiProperty({
    description: '趣味',
    type: Number,
    isArray: true,
  })
  @IsArray({ each: true })
  @IsNumber({}, { each: true }) ⇦ 配列の中身が全てNumber型であることを保証します
  hobbyIds: number[]
  
  @ApiProperty({
    description: '誕生日',
    type: String, ⇦ リクエスト時の型を指定するため、「String」です!
  })
  @Transform(({ value }) => startOfDay(value))
  @IsDate()
  @Type(() => Date)
  birthDay: Date

  @ApiProperty({
    description: '休日の過ごし方',
    type: WeekendActivitiesDto,
  })
  @ValidateNested()
  @IsObject()
  @Type(() => WeekendActivitiesDto) ⇦ TypeScriptの型情報は実行時に消えてしまうため必須!!
  weekendActivities: WeekendActivitiesDto
}

最後に

記事の中でも「解答例」という書き方をしましたが、あくまで実装方法は一例です!もっとシンプルな書き方や詳細な書き方があるかもしれません!
また、アウトプットで問題が簡単に解けた人は「既存のDTO」⇨「問題の作成」をやってみると、より理解が進むと思います!よかったらやってみてください!

株式会社シンシア

株式会社xincereでは、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら

シンシアでは、年間100人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link