PATCHエンドポイントの作り方
全体の流れ
- ベーススキーマを1つ定義する
- そこから partial().required({ id: true }) で UPDATE用スキーマを派生させる
- ハンドラ内で safeParse してバリデーション
- ...スプレッド で送られたフィールドだけ上書き
ベーススキーマの定義
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), age: z.number().optional(), });
UPDATE用スキーマを派生
const UpdateUserSchema = UserSchema .partial() // 全フィールドをoptionalに .required({ id: true }); // idだけ必須に戻す
これで「どのフィールドの組み合わせで送ってもいいが、idは必ず必要」という型が完成する。
ハンドラの実装
function updateUser(rawBody: unknown) { // 1. バリデーション const result = UpdateUserSchema.safeParse(rawBody); if (!result.success) { return { status: 400, errors: result.error.issues }; }
// 2. 対象レコードを探す
const index = db.findIndex(u => u.id === result.data.id);
if (index === -1) {
return { status: 404, error: "Not Found" };
}
// 3. 送られたフィールドだけ上書き
db[index] = { ...db[index], ...result.data };
return { status: 200, data: db[index] };
}
なぜこれで動くか
// 既存データ db[index] = { id: 1, name: "田中", email: "t@example.com", age: 30 };
// 送られてきたデータ (nameだけ) result.data = { id: 1, name: "鈴木" };
// スプレッドで合成 (後勝ち) { ...db[index], ...result.data } // → { id: 1, name: "鈴木", email: "t@example.com", age: 30 } // ↑ 上書き ↑ 送られなかったフィールドは既存値を維持
partial() が「送らなくていい」を型で保証し、スプレッドが「送られたものだけ上書き」を実装する。こ の2つがセットでPATCHが完成する。
UserSchemaにフィールドを追加したとき
const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), age: z.number().optional(), role: z.enum(["admin", "user"]), // ← 追加 });
UpdateUserSchema は UserSchema から派生しているので自動で追従する。修正箇所はベーススキーマの1箇所だけ。