Next.js API Routes に Zod を組み込む
バリデーションライブラリである Zod を、Next.js で活用する TIPS の紹介です。筆者が Zod を知り・使い始めたのは、React Hook Form のリゾルバーがきっかけです。ブラウザでバリデーションを行うので、不正な入力値検証を API リクエストが発生する前に実行できます。
この Zod はフロントだけではなく、サーバープロセスでも使用できます。例えば、tRPC・Zodiosなどに見られるように、サーバーへのリクエスト(入力値)を検証しつつ型推論も解決してくれるソリューションとして注目されています。
Next.js API Routes に Zod を組み込む
Next.js には REST API の実装手段として、API Routes が提供されています。しかし、req
に含まれる入力値検証は自前で用意する必要があります。この入力値検証に Zod を使用されている方も多いのではないでしょうか。今回紹介するのは Zod の入力値バリデーションを行いつつ、API Routes ハンドラーに型推論を適用する方法です。
以下のサンプルにあるwithZod
関数がその例です。withZod(ZodSchema, NextApiHandler)
のように定義することで、第一引数に定義したスキーマどおりの入力値が、req
の型推論に適用されます。もちろん、バリデーションを通過しない場合 400 レスポンスを返します。
import { withZod } from "@/lib/next/withZod";
import { NextApiHandler } from "next";
import { z } from "zod";
const handleGet = withZod(
// req.query, req.body に期待するバリデーションスキーマを定義
z.object({
query: z.object({ name: z.string() }),
}),
async (req, res) => {
// req の型推論がスキーマどおりとなる
const name = req.query.name; // const name: string
res.status(200).json({ message: `Hello ${name}` });
}
);
const handler: NextApiHandler = async (req, res) => {
switch (req.method) {
case "GET":
return handleGet(req, res);
default:
res.status(405).json({ message: "Method Not Allowed" });
return;
}
};
export default handler;
withZod 関数の内訳
高階関数であるwithZod
は、内部でschema.safeParse(req)
を実行しています。第二引数である後続のNextApiHandler
が実行される前に 400 レスポンスを返却しているので、不正な入力値の場合、永続層へのアクセスを未然に防ぐことができます。
import { NextApiRequest, NextApiResponse } from "next";
import { z, ZodSchema } from "zod";
export function withZod<T extends ZodSchema>(
schema: T,
next: (
// NextApiRequest に定義されている曖昧な "query" | "body" 定義を除去し、
// z.infer でスキーマの型定義を抽出する
req: Omit<NextApiRequest, "query" | "body"> & z.infer<T>,
res: NextApiResponse
) => unknown | Promise<unknown>
) {
return async (req: NextApiRequest, res: NextApiResponse) => {
const parsed = schema.safeParse(req);
if (!parsed.success) {
// 共通のバリデーションエラーレスポンスとして処理
res.status(400).json({
message: "Bad Request",
issues: JSON.parse(parsed.error.message),
});
return;
}
return next(req, res);
};
}
入力値のさらなる検証
冒頭のハンドラー例では、?name=text
のリクエストのみ通過するようにスキーマが定義されていました。この入力値検証は、次のようにquery
・body
両方が満たされているようにもできます。要件にあわせて Zod スキーマを定義すればよいため、後続のNextApiHandler
処理がシンプルになります。
const handlePost = withZod(
z.object({
query: z.object({ id: z.string().min(8) }),
body: z.object({ message: z.string().min(1) }),
}),
async (req, res) => {
res
.status(200)
.json({ message: `ID: ${req.query.id}, message: ${req.body.message}` });
// req.query.id, req.body.message は型推論が効いている
}
);
query の型変換
query の検証には注意が必要です。以下のquery.id
は「正の整数」を期待したスキーマですが、req.query
は必ずPartial<{ [key: string]: string | string[] }>
になります。そのため、このバリデーションは通過しません。
z.object({
query: z.object({ id: z.number().positive().int() }),
});
req.query.id
が「正の整数」であると判定するためには、以下のようにrefine
で判定式を書く必要があります。また、バリデーションが通過したら number 型として扱いたいはずなので、transform
で変換しておきます。これで後続のNextApiHandler
ではreq.query.id
を「正の整数」として扱えるようになりました。
z.object({
query: z.object({
id: z
.string()
.refine((v) => Number.isInteger(+v) && +v > 0, "Require positive int.")
.transform((v) => +v),
}),
});
まとめ
Zod はバリデーション・型推論に特化しているため、アプリケーション設計に組み込むことができます。Next.js は薄いフレームワークですので、Zod をうまく織り交ぜていくと便利です。
Discussion
大変参考になる記事でした。ありがとうございます。
一点、気づいた点があったためコメントさせていただきます。
queryの型変換について、記事同様に実装してみても
req.query
は型のみ変更されていて、値は変換されていないようです。そのため、以下の実装で対応しました。