💎

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...
}

https://nextjs.org/docs/app/api-reference/file-conventions/page#props

実際のユースケースではここから渡ってきた値を使ってリソースを取得することが多いです。
ただ、 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] }

https://github.com/colinhacks/zod/discussions/1869#discussioncomment-4669474

まとめ

自分へのメモ書きも兼ねてよくあるユースケースをまとめました。

参考になれば幸いです。

Discussion