この記事の対象読者
- フォーム実装をする人
- フロントとAPIで入力チェックが二重管理になっている人
- Zodなどのスキーマバリデーションを使ってみたい人
本題に入る前に
画面では保存できそうに見えたのに、APIでバリデーションエラーになる。逆に、画面では止めているけれどAPIでは受けてしまう。
こういうズレは、最初は小さく見えます。でも、フォーム項目が増えたり、管理画面が増えたりすると、あとから効いてきます。
入力チェックには2つの目的があります。
- ユーザーが入力ミスに気づきやすくする
- APIやDBに不正なデータを入れない
この記事では、TypeScriptとZodを例に、画面とAPIの入力チェックをズレにくくする考え方を整理します。
フロントだけのチェックでは足りない
フロントエンドのバリデーションは、ユーザー体験のために大事です。
ただし、API側のチェックも必要です。フロントを通らないリクエスト、古い画面、別クライアントなどがあるからです。
画面では早めに気づけるようにする。APIでは最後に守る。役割を分けると考えやすいです。
Zodでスキーマを作る
予約作成の入力を例にします。
import { z } from "zod";
export const CreateReservationSchema = z.object({
title: z.string().min(1, "タイトルを入力してください"),
startsAt: z.string().datetime("日時の形式が正しくありません"),
userId: z.string().min(1),
});
export type CreateReservationInput = z.infer<typeof CreateReservationSchema>;
API側では safeParse します。
async function createReservationHandler(body: unknown) {
const result = CreateReservationSchema.safeParse(body);
if (!result.success) {
return {
status: 400,
body: {
errorCode: "VALIDATION_ERROR",
issues: result.error.issues,
},
};
}
const input = result.data;
return {
status: 201,
body: {
id: "reservation_001",
title: input.title,
},
};
}
これで、APIに来たデータが期待した形かを確認できます。
画面では文言を使いやすくする
APIのエラーをそのまま画面に出すと、少し硬いことがあります。
画面では、ユーザーに分かる文言に変換します。
function toFieldErrors(issues: Array<{ path: unknown[]; message: string }>) {
return issues.reduce<Record<string, string>>((acc, issue) => {
const key = String(issue.path[0]);
acc[key] = issue.message;
return acc;
}, {});
}
画面側の表示イメージです。
タイトル: タイトルを入力してください
開始日時: 日時の形式が正しくありません
APIのスキーマを中心に置きつつ、画面では文言の見せ方を整えると使いやすくなります。
共有スキーマの注意点
フロントとバックエンドで同じZodスキーマを共有できると便利です。
ただし、何でも共有すればよいわけではありません。
- APIで必須だが、画面では段階的に入力する項目
- 管理画面とユーザー画面で文言を変えたい項目
- API内部だけで使う項目
こうしたものは、共通スキーマに寄せすぎると逆に扱いづらくなることがあります。
「データとして正しいか」と「画面でどう入力させるか」は、近いけれど同じではありません。
まとめ
入力チェックは、画面だけでもAPIだけでも足りません。
画面では入力ミスに早く気づけるようにする。APIでは不正なデータを最後に止める。Zodのようなスキーマを使うと、この2つをズレにくくできます。
まずは1つのフォームで、API側のスキーマを作るところから始めるのがおすすめです。