22
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-04-07

はじめに

今回の記事では、 「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の実装は任意です

必要なリクエスト内容

  • ユーザー名 (name: string)

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

  • 年齢 (age: number)

解答例
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人程度の実務未経験の方が応募し技術面接を受けます。
その経験を通し、実務未経験者の方にぜひ身につけて欲しい技術力(文法)をここでは紹介していきます。

22
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?