Skip to content

PATCHエンドポイントの作り方

全体の流れ

  1. ベーススキーマを1つ定義する
  2. そこから partial().required({ id: true }) で UPDATE用スキーマを派生させる
  3. ハンドラ内で safeParse してバリデーション
  4. ...スプレッド で送られたフィールドだけ上書き

ベーススキーマの定義

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箇所だけ。