NestJSでzodを使う！基本的な使い方を整理してみた。

## はじめに

TypeScriptでのフルスタック開発ではzodを使うことがデファクトっぽい。（？）
初めてzodを使ったので、備忘録として基本的な使い方をまとめてみました。

### 目次

- スキーマと型の一元管理  
- DTOの自動生成  
- バリデーションの実装  
- PrismaなどのORMとの連携  

---

## 1. **用語**　スキーマと型の違い

| 用語      | 役割・目的                           | 使われるタイミング         |
|-----------|--------------------------------------|---------------------------|
| スキーマ  | データ構造とバリデーションルール定義 | 実行時（バリデーション）  |
| 型        | データの型安全性を保証               | 開発時（型チェック）      |

- **スキーマ**はZodなどで定義し、実行時にデータの検証や変換を行います。
- **型**はTypeScriptの型システムで、開発時の型安全性を担保します。

### 例

```typescript
import { z } from 'zod';

const userSchema = z.object({
  id: z.string(),
  name: z.string(),
});

type User = z.infer<typeof userSchema>;
```

---

## 2. DTOとスキーマの違い

- **DTO（Data Transfer Object）**は、層間やAPIの入出力で使う「データの型定義」。
- **スキーマ**は、DTOの型を生成しつつ、バリデーションも担う「実行時の検証ルール」。

**現代的な設計では、ZodスキーマからDTO型を自動生成し、型とバリデーションを一元管理するのがベストプラクティスです。**

---

## 3. Zodの便利なメソッド

### pick

指定したフィールドだけ抜き出す

```typescript
const createUserSchema = userSchema.pick({ name: true });
type CreateUserDto = z.infer<typeof createUserSchema>;
// => { name: string }
```

### omit

指定したフィールドを除外

```typescript
const userWithoutIdSchema = userSchema.omit({ id: true });
type UserWithoutId = z.infer<typeof userWithoutIdSchema>;
// => { name: string }
```

### partial

全フィールドを「任意（optional）」に

```typescript
const userPartialSchema = userSchema.partial();
type UserPartial = z.infer<typeof userPartialSchema>;
// => { id?: string; name?: string }
```

---

## 4. サンプル：UserとProfileのCRUD API設計

### 1. スキーマ定義と型生成

`userWithProfileSchema`は、User型にprofileプロパティを追加した複合型として定義しています。
`profileSchema.omit({ userId: true })`で、結合時に不要なuserIdを除外しています。


```typescript
import { z } from 'zod';

export const userSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

export const profileSchema = z.object({
  id: z.string(),
  userId: z.string(),
  age: z.number().min(0),
  bio: z.string().optional(),
});

export const userWithProfileSchema = userSchema.extend({
  profile: profileSchema.omit({ userId: true }),
});

export type User = z.infer<typeof userSchema>;
export type Profile = z.infer<typeof profileSchema>;
export type UserWithProfile = z.infer<typeof userWithProfileSchema>;
```

### 2. DTO生成

APIのリクエストやレスポンスで使うDTO（Data Transfer Object）は、
必要なフィールドだけを抜き出したり、バリデーションルールを追加したりしてスキーマを作成します。

```typescript
export const createUserProfileSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0),
  bio: z.string().optional(),
});
export type CreateUserProfileDto = z.infer<typeof createUserProfileSchema>;

export const updateUserProfileSchema = createUserProfileSchema.partial();
export type UpdateUserProfileDto = z.infer<typeof updateUserProfileSchema>;
```

### 3. Zodバリデーションパイプ

コントローラーで`@UsePipes(new ZodValidationPipe(schema))`と使うことで、
そのエンドポイントのリクエストボディがZodスキーマでバリデーションされます。
バリデーションエラー時は自動で400エラーが返ります。

https://docs.nestjs.com/pipes

```typescript
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common';
import { ZodSchema, ZodError } from 'zod';

@Injectable()
export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema<any>) {}

  transform(value: unknown) {
    try {
      return this.schema.parse(value);
    } catch (error) {
      if (error instanceof ZodError) {
        throw new BadRequestException(error.errors.map(e => e.message).join(', '));
      }
      throw error;
    }
  }
}
```

### 4. コントローラーの実装サンプル

NestJSのコントローラーで、DTOとバリデーションパイプを使ってCRUD APIを実装するサンプルです。

```typescript
import { Controller, Get, Post, Body, Param, Patch, Delete, UsePipes } from '@nestjs/common';
import { UserProfileService } from '../services/user-profile.service';
import {
  createUserProfileSchema,
  CreateUserProfileDto,
  updateUserProfileSchema,
  UpdateUserProfileDto,
} from '../dto/user-profile.dto';
import { ZodValidationPipe } from '../pipes/zod-validation.pipe';

@Controller('users')
export class UserProfileController {
  constructor(private readonly service: UserProfileService) {}

  @Post()
  @UsePipes(new ZodValidationPipe(createUserProfileSchema))
  create(@Body() dto: CreateUserProfileDto) {
    return this.service.create(dto);
  }

  @Get()
  findAll() {
    return this.service.findAll();
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return this.service.findOne(id);
  }

  @Patch(':id')
  @UsePipes(new ZodValidationPipe(updateUserProfileSchema))
  update(@Param('id') id: string, @Body() dto: UpdateUserProfileDto) {
    return this.service.update(id, dto);
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return this.service.remove(id);
  }
}
```

---

## 5. まとめ

基本的な使い方を整理してみました。
いろんな記事を参照しつつAIにサンプルコードを実装してもらうことで効率よく学習ができました。
参考記事はこちら

https://zod.dev/basics

https://zenn.dev/terrierscript/books/2023-01-typed-zod

https://tech.kikagaku.co.jp/entry/2024/03/22/170000

https://speakerdeck.com/yosashusaku/nestjs-trpctozhan-shu-de-dddnoiitokodoriwositebatukuendotypescriptnoshe-ji-wositahua?slide=18