💎
Next.js の params と searchParams を Zod で型安全に取り出す
はじめに
Next.js ではパスパラメーターは params で、クエリパラメーターは searchParams で取り出せます。
src/app/articles/[slug]/page.tsx
export default async function Page({
params,
searchParams
}: {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
// code...
}
実際のユースケースではここから渡ってきた値を使ってリソースを取得することが多いです。
ただ、 URL はユーザーが任意に入力できるため、想定された書式となっているかチェックを入れて取り出したいです。
Zod でパラメーターチェック用の Schema を定義する
Schema Validation ができればどんなライブラリでも OK ですが、Zod を使うことにします。
params の Validation
パスパラメーターは String で渡されるのでそれが指定の書式かチェックします。
src/app/todos/[id]/page.tsx
import { notFound } from "next/navigation";
import { z } from "zod/v4";
const paramsSchema = z.object({
id: z.coerce.number(), // String を Number に変換する
// id: z.uuid(), // UUID の場合
});
export default async function Page({
params,
}: {
params: Promise<{ id: string }>;
}) {
const parsed = paramsSchema.safeParse(await params);
if (!parsed.success) {
notFound();
}
// fetch resource...
return (
<section>
<pre>{JSON.stringify(parsed.data)}</pre>
</section>
);
}
searchParams の Validation
クエリパラメーターはパスパラメーターよりも多様なことが多いため、色々なユースケースに合わせて取り出します。
ポイントとしては必須かどうかで optional() 指定を変えます。
src/app/search/page.tsx
import { notFound } from "next/navigation";
import { z } from "zod/v4";
const searchParamsSchema = z.object({
q: z.string().min(1), // 必須
completed: z.optional(z.enum(["true", "false"])), // 任意
});
export default async function Page({
searchParams,
}: {
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}) {
const parsed = searchParamsSchema.safeParse(await searchParams);
if (!parsed.success) {
notFound();
}
// /search?q=something&completed=true&foo=bar
// => { "q": "something", "completed": "true" }
// { "foo": "bar" } は無視される
return (
<section>
<pre>{JSON.stringify(parsed.data)}</pre>
</section>
);
}
不要な値があったら弾きたい
前述の例では規定されていないクエリパラメーターは無視していますが、ユースケースとして、不正な値があったら弾きたい場合があります。
その場合は .strict() を指定します。
src/app/search/page.tsx
const parsed = searchParamsSchema.strict().safeParse(await searchParams);
// /search?q=something&completed=true&foo=bar
// 規定されていない `{ "foo": "bar" }` があるため、パース失敗する
if (!parsed.success) {
notFound();
}
規定ではない値は Fallback しておきたい
あるユースケースでは規定ではない場合でもエラーにはせず処理したい場合があります。
その場合は .catch() でエラー時に丸める値を規定します。
src/app/invoices/page.tsx
// /invoices?start=2025-01-01&end=2025-12-31
// => {"start":"2025-01-01","end":"2025-12-31"}
// /invoices?start=2025-01-01&end=2025-12
// => {"start":"2025-01-01","end":""}
const searchParamsSchema = z.object({
start: z.optional(z.iso.date()).catch(""),
end: z.optional(z.iso.date()).catch(""),
});
パラメーターが配列値の場合
例えばカンマ区切りで複数の値がある場合でも、 Input は String 型です。
配列として取り扱う場合、 .transform() で配列に変換して、.pipe() でチェックします。
src/app/todos/page.tsx
const searchParamsSchema = z.object({
ids: z
.string()
.transform((value) => value.split(",").map((v) => Number(v)))
.pipe(z.number().array()),
});
// /todos?ids=1,3,5
// => {"ids": [1,3,5] }
まとめ
自分へのメモ書きも兼ねてよくあるユースケースをまとめました。
参考になれば幸いです。
Discussion