はじめに
TypeScriptでのフルスタック開発ではzodを使うことがデファクトっぽい。(?)
初めてzodを使ったので、備忘録として基本的な使い方をまとめてみました。
目次
- スキーマと型の一元管理
- DTOの自動生成
- バリデーションの実装
- PrismaなどのORMとの連携
1. 用語 スキーマと型の違い
用語 | 役割・目的 | 使われるタイミング |
---|---|---|
スキーマ | データ構造とバリデーションルール定義 | 実行時(バリデーション) |
型 | データの型安全性を保証 | 開発時(型チェック) |
- スキーマはZodなどで定義し、実行時にデータの検証や変換を行います。
- 型は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
指定したフィールドだけ抜き出す
const createUserSchema = userSchema.pick({ name: true });
type CreateUserDto = z.infer<typeof createUserSchema>;
// => { name: string }
omit
指定したフィールドを除外
const userWithoutIdSchema = userSchema.omit({ id: true });
type UserWithoutId = z.infer<typeof userWithoutIdSchema>;
// => { name: string }
partial
全フィールドを「任意(optional)」に
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を除外しています。
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)は、
必要なフィールドだけを抜き出したり、バリデーションルールを追加したりしてスキーマを作成します。
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エラーが返ります。
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を実装するサンプルです。
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にサンプルコードを実装してもらうことで効率よく学習ができました。
参考記事はこちら