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

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

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

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

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

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

## 各内容へのリンク

#### インプット編
- [ドキュメント化](#1-ドキュメント化)
- [型定義](#2-型定義-下記で定義した以外の値を受け取るとエラーになる)
- [バリデーション](#3-バリデーション)
- [型変換、値のカスタム](#4--型変換値のカスタム)

#### アウトプット編
- [例題1](#例題1-searchuserdto)
- [例題2](#例題2-createuserdto)
- [練習問題(1)](#練習問題1-searchpostdto)
- [練習問題(2)](#練習問題2-createprofiledto)

## インプット編
### 1. ドキュメント化

```typescript
@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)

<details>
<summary>解答例</summary>

```typescript
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;
}
```

</details>

<details>
<summary>実装のポイント</summary>

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

</details>

#### 例題(2) 【CreateUserDto】

##### 要件

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

#### 必要なリクエスト内容

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

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

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

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

<details>
<summary>解答例</summary>

```typescript
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;
}
```

</details>

<details>
<summary>実装のポイント</summary>

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

</details>

#### 練習問題(1) 【SearchPostDto】

##### 要件

- 投稿の検索DTOを実装したい
- `@ApiProperty`の実装は**必須**とします

#### 必要なリクエスト内容

- 投稿名 (name: string)
- 投稿番号 (code: string)
- ユーザーID (userId: number)
- コメントID (commentId: number)
- お気に入りID (favoriteId: number)

<details>
<summary>解答例</summary>

```typescript
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
}
```

</details>

#### 練習問題(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」はオブジェクトです

<details>
<summary>解答例</summary>

```typescript
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
}
```

</details>

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

## 株式会社シンシア
<a href="https://corp.xincere.jp/" target="_blank" rel="noopener noreferrer">株式会社xincere</a>では、実務未経験のエンジニアの方や学生エンジニアインターンを採用し一緒に働いています。
※ シンシアにおける働き方の様子はこちら

https://www.wantedly.com/companies/xincere-inc/stories

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