Dev Study
← 解説「DTO — リクエストの形を定義する」に戻る

サンプルコードで身につける: DTO — リクエストの形を定義する

解説で学んだ概念を、5つの具体例で確認します。素朴な例から実務寄りの例へと進みます。

1最小のDTO

インラインの型注釈を専用クラスに昇格させた最小の DTO です。「POST /tasks のボディの形」に CreateTaskDto という名前が付き、コントローラとサービスの両方から同じ形を参照できるようになります。

import { Body, Controller, Post } from "@nestjs/common";

// 「POST /tasks のボディの形」に名前を付ける
export class CreateTaskDto {
  title: string;
  deadline: string;
}

@Controller("tasks")
export class TasksController {
  @Post()
  create(@Body() dto: CreateTaskDto) {
    return { created: dto.title };
  }
}

2操作ごとにDTOを分ける

作成用と更新用で DTO を分ける定番パターンです。作成は全項目必須、更新は変えたい項目だけ送ればよい、という API 仕様の違いを型のレベルでそのまま表現できます。

// 作成時: 全項目が必須
export class CreateUserDto {
  name: string;
  email: string;
  age: number;
}

// 更新時: 変えたい項目だけ送ればよいので全項目オプション
export class UpdateUserDto {
  name?: string;
  email?: string;
  age?: number;
}

// 「作成と更新で必須項目が違う」という仕様を型で表現できる
// 操作名 + Dto という命名で、用途がファイル名から分かる

3class は実行時に残る

DTO を interface ではなく class で書く理由を実行して確かめる例です。class はコンパイル後も値として残るため実行時に参照できますが、interface は消えてしまいます。この性質が次レッスンの検証の土台になります。

TypeScript

4ネストしたDTO

DTO の中に別の DTO を入れ子にする例です。住所のような階層を持つ JSON も、オブジェクトのプロパティと配列でそのまま型にできます。深い構造は小さな DTO の組み合わせで表すのが読みやすさの定石です。

export class AddressDto {
  postalCode: string;
  city: string;
}

export class CreateCompanyDto {
  companyName: string;
  address: AddressDto;    // DTO の中に DTO を入れ子にできる
  branches: AddressDto[]; // 配列もそのまま表現できる
}

// 対応する JSON:
// {
//   "companyName": "Example社",
//   "address": { "postalCode": "100-0001", "city": "千代田区" },
//   "branches": [{ "postalCode": "530-0001", "city": "大阪市" }]
// }

5ページネーション用のクエリDTO

一覧系 API で使い回すページネーション DTO の実務例です。ボディだけでなく @Query() の型注釈にも DTO を使えること、クエリ由来の値は常に文字列で届くことを合わせて示します。

import { Controller, Get, Query } from "@nestjs/common";

// 一覧系 API すべてで使い回す定番 DTO(クエリ文字列用)
export class PaginationQueryDto {
  page?: string;  // クエリ由来の値は常に文字列で届く
  limit?: string; // (数値への変換は後のレッスンで)
}

@Controller("articles")
export class ArticlesController {
  @Get() // GET /articles?page=2&limit=10
  findAll(@Query() query: PaginationQueryDto) {
    return {
      page: query.page ?? "1",
      limit: query.limit ?? "20",
    };
  }
}