{"id":14957351,"url":"https://github.com/lukemorales/next-safe-navigation","last_synced_at":"2025-07-24T06:36:29.136Z","repository":{"id":220379490,"uuid":"751491507","full_name":"lukemorales/next-safe-navigation","owner":"lukemorales","description":"Static type and runtime validation for navigating routes in NextJS App Router with Zod schemas","archived":false,"fork":false,"pushed_at":"2024-08-17T20:32:29.000Z","size":251,"stargazers_count":172,"open_issues_count":4,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-03T21:57:01.974Z","etag":null,"topics":["app-router","app-router-nextjs","navigation","next","next-js","nextjs","runtime-validation","static-typechecking","type-safety","typescript","validation","vercel","zod"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/next-safe-navigation","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lukemorales.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-01T18:02:26.000Z","updated_at":"2025-02-12T21:40:37.000Z","dependencies_parsed_at":"2024-03-27T01:24:24.763Z","dependency_job_id":"d253cfa7-694b-40ea-9ae5-53d6f8681d66","html_url":"https://github.com/lukemorales/next-safe-navigation","commit_stats":{"total_commits":35,"total_committers":4,"mean_commits":8.75,"dds":0.2571428571428571,"last_synced_commit":"ddc27ea7ed1d047e84b95734c7ae8603dc523401"},"previous_names":["lukemorales/safe-next-navigation","lukemorales/next-safe-navigation"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukemorales%2Fnext-safe-navigation","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukemorales%2Fnext-safe-navigation/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukemorales%2Fnext-safe-navigation/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lukemorales%2Fnext-safe-navigation/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lukemorales","download_url":"https://codeload.github.com/lukemorales/next-safe-navigation/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244047646,"owners_count":20389206,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["app-router","app-router-nextjs","navigation","next","next-js","nextjs","runtime-validation","static-typechecking","type-safety","typescript","validation","vercel","zod"],"created_at":"2024-09-24T13:14:46.662Z","updated_at":"2025-07-24T06:36:29.124Z","avatar_url":"https://github.com/lukemorales.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/lukemorales/next-safe-navigation\" target=\"\\_parent\"\u003e\u003cimg src=\"https://em-content.zobj.net/source/apple/354/goggles_1f97d.png\" alt=\"Goggles emoji\" height=\"130\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003ch1 align=\"center\"\u003eSafe NextJS Navigation\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/lukemorales/next-safe-navigation/actions/workflows/tests.yml\" target=\"\\_parent\"\u003e\u003cimg src=\"https://github.com/lukemorales/next-safe-navigation/actions/workflows/tests.yml/badge.svg?branch=main\" alt=\"Latest build\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://codecov.io/gh/lukemorales/next-safe-navigation\"\u003e\u003cimg src=\"https://codecov.io/gh/lukemorales/next-safe-navigation/graph/badge.svg?token=35GW5EJMFK\"/\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/next-safe-navigation\" target=\"\\_parent\"\u003e\u003cimg src=\"https://badgen.net/npm/v/next-safe-navigation\" alt=\"Latest published version\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/package/next-safe-navigation@latest\" target=\"\\_parent\"\u003e\u003cimg src=\"https://badgen.net/bundlephobia/minzip/next-safe-navigation\" alt=\"Bundlephobia\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://bundlephobia.com/package/next-safe-navigation@latest\" target=\"\\_parent\"\u003e\u003cimg src=\"https://badgen.net/bundlephobia/tree-shaking/next-safe-navigation\" alt=\"Tree shaking available\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/lukemorales/next-safe-navigation\" target=\"\\_parent\"\u003e\u003cimg src=\"https://badgen.net/npm/types/next-safe-navigation\" alt=\"Types included\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/next-safe-navigation\" target=\"\\_parent\"\u003e\u003cimg src=\"https://badgen.net/npm/license/next-safe-navigation\" alt=\"License\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/next-safe-navigation\" target=\"\\_parent\"\u003e\u003cimg src=\"https://badgen.net/npm/dt/next-safe-navigation\" alt=\"Number of downloads\"\u003e\u003c/a\u003e\n  \u003ca href=\"https://github.com/lukemorales/next-safe-navigation\" target=\"\\_parent\"\u003e\u003cimg src=\"https://img.shields.io/github/stars/lukemorales/next-safe-navigation.svg?style=social\u0026amp;label=Star\" alt=\"GitHub Stars\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cstrong\u003eStatic type and runtime validation for navigating routes in \u003ca href=\"https://nextjs.org\" target=\"\\_parent\"\u003eNextJS App Router\u003c/a\u003e with Standard Schema support (Zod, Valibot, ArkType, etc.).\u003c/strong\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  Static and runtime validation of routes, route params and query string parameters on client and server components.\n\u003c/p\u003e\n\n## 📦 Install\n\nSafe NextJS Navigation is available as a package on NPM, install with your favorite package manager:\n\n```dircolors\nnpm install next-safe-navigation\n```\n\nYou'll also need to install a Standard Schema compatible validation library:\n\n```dircolors\n# Choose one:\nnpm install zod           # Zod (most popular)\nnpm install valibot       # Valibot (lightweight)\nnpm install arktype       # ArkType (fast)\n```\n\n## ⚡ Quick start\n\n\u003e [!TIP]\n\u003e Enable `experimental.typedRoutes` in `next.config.js` for a better and safer experience with autocomplete when defining your routes\n\n### Declare your application routes and parameters in a single place\n\n```ts\n// src/shared/navigation.ts\nimport { createNavigationConfig } from 'next-safe-navigation';\nimport { z } from 'zod';\n\nexport const { routes, useSafeParams, useSafeSearchParams } =\n  createNavigationConfig((defineRoute) =\u003e ({\n    home: defineRoute('/'),\n    customers: defineRoute('/customers', {\n      search: z\n        .object({\n          query: z.string().default(''),\n          page: z.coerce.number().default(1),\n        })\n        .default({ query: '', page: 1 }),\n    }),\n    invoice: defineRoute('/invoices/[invoiceId]', {\n      params: z.object({\n        invoiceId: z.string(),\n      }),\n    }),\n    shop: defineRoute('/support/[...tickets]', {\n      params: z.object({\n        tickets: z.array(z.string()),\n      }),\n    }),\n    shop: defineRoute('/shop/[[...slug]]', {\n      params: z.object({\n        // ⚠️ Remember to always set your optional catch-all segments\n        // as optional values, or add a default value to them\n        slug: z.array(z.string()).optional(),\n      }),\n    }),\n  }));\n```\n\n### Runtime validation for React Server Components (RSC)\n\n\u003e [!IMPORTANT]\n\u003e The output of a schema might not be the same as its input, since schemas can transform the values during parsing (e.g.: string to number coercion), especially when dealing with `URLSearchParams` where all values are strings and you might want to convert params to different types. For this reason, this package does not expose types to infer `params` or `searchParams` from your declared routes to be used in page props:\n\u003e\n\u003e ```ts\n\u003e interface CustomersPageProps {\n\u003e   // ❌ Do not declare your params | searchParam types\n\u003e   searchParams?: ReturnType\u003ctypeof routes.customers.$parseSearchParams\u003e;\n\u003e }\n\u003e ```\n\u003e\n\u003e Instead, it is strongly advised that you parse the params in your server components to have runtime validated and accurate type information for the values in your app.\n\n```ts\n// src/app/customers/page.tsx\nimport { routes } from \"@/shared/navigation\";\n\ninterface CustomersPageProps {\n  // ✅ Never assume the types of your params before validation\n  searchParams?: unknown\n}\n\nexport default async function CustomersPage({ searchParams }: CustomersPageProps) {\n  const { query, page } = routes.customers.$parseSearchParams(searchParams);\n\n  const customers = await fetchCustomers({ query, page });\n\n  return (\n    \u003cmain\u003e\n      \u003cinput name=\"query\" type=\"search\" defaultValue={query} /\u003e\n\n      \u003cCustomers data={customers} /\u003e\n    \u003c/main\u003e\n  )\n};\n\n/* --------------------------------- */\n\n// src/app/invoices/[invoiceId]/page.tsx\nimport { routes } from \"@/shared/navigation\";\n\ninterface InvoicePageProps {\n  // ✅ Never assume the types of your params before validation\n  params?: unknown\n}\n\nexport default async function InvoicePage({ params }: InvoicePageProps) {\n  const { invoiceId } = routes.invoice.$parseParams(params);\n\n  const invoice = await fetchInvoice(invoiceId);\n\n  return (\n    \u003cmain\u003e\n      \u003cInvoice data={customers} /\u003e\n    \u003c/main\u003e\n  )\n};\n```\n\n### Runtime validation for Client Components\n\n```ts\n// src/app/customers/page.tsx\n'use client';\n\nimport { useSafeSearchParams } from \"@/shared/navigation\";\n\nexport default function CustomersPage() {\n  const { query, page } = useSafeSearchParams('customers');\n\n  const customers = useSuspenseQuery({\n    queryKey: ['customers', { query, page }],\n    queryFn: () =\u003e fetchCustomers({ query, page}),\n  });\n\n  return (\n    \u003cmain\u003e\n      \u003cinput name=\"query\" type=\"search\" defaultValue={query} /\u003e\n\n      \u003cCustomers data={customers.data} /\u003e\n    \u003c/main\u003e\n  )\n};\n\n/* --------------------------------- */\n\n// src/app/invoices/[invoiceId]/page.tsx\n'use client';\n\nimport { useSafeParams } from \"@/shared/navigation\";\n\nexport default function InvoicePage() {\n  const { invoiceId } = useSafeParams('invoice');\n\n  const invoice = useSuspenseQuery({\n    queryKey: ['invoices', { invoiceId }],\n    queryFn: () =\u003e fetchInvoice(invoiceId),\n  });\n\n  return (\n    \u003cmain\u003e\n      \u003cInvoice data={invoice.data} /\u003e\n    \u003c/main\u003e\n  )\n};\n```\n\nUse throughout your codebase as the single source for navigating between routes:\n\n```ts\nimport { routes } from \"@/shared/navigation\";\n\nexport function Header() {\n  return (\n    \u003cnav\u003e\n      \u003cLink href={routes.home()}\u003eHome\u003c/Link\u003e\n      \u003cLink href={routes.customers()}\u003eCustomers\u003c/Link\u003e\n    \u003c/nav\u003e\n  )\n};\n\nexport function CustomerInvoices({ invoices }) {\n  return (\n    \u003cul\u003e\n      {invoices.map(invoice =\u003e (\n        \u003cli key={invoice.id}\u003e\n          \u003cLink href={routes.invoice({ invoiceId: invoice.id })}\u003e\n            View invoice\n          \u003c/Link\u003e\n        \u003c/li\u003e\n      ))}\n    \u003c/ul\u003e\n  )\n};\n```\n\n## 🔄 Standard Schema Support\n\nThis library now supports [Standard Schema](https://github.com/standard-schema/standard-schema), which means you can use any compatible validation library:\n\n### Using Zod\n\n```ts\n// src/shared/navigation.ts\nimport { createNavigationConfig } from 'next-safe-navigation';\nimport { z } from 'zod';\n\nexport const { routes, useSafeParams, useSafeSearchParams } =\n  createNavigationConfig((defineRoute) =\u003e ({\n    customers: defineRoute('/customers', {\n      search: z\n        .object({\n          query: z.string().default(''),\n          page: z.coerce.number().default(1),\n        })\n        .default({ query: '', page: 1 }),\n    }),\n    invoice: defineRoute('/invoices/[invoiceId]', {\n      params: z.object({\n        invoiceId: z.string(),\n      }),\n    }),\n  }));\n```\n\n### Using Valibot\n\n```ts\n// src/shared/navigation.ts\nimport { createNavigationConfig } from 'next-safe-navigation';\nimport * as v from 'valibot';\n\nexport const { routes, useSafeParams, useSafeSearchParams } =\n  createNavigationConfig((defineRoute) =\u003e ({\n    customers: defineRoute('/customers', {\n      search: v.objectWithRest(\n        {\n          query: v.optional(v.string(), ''),\n          page: v.optional(v.pipe(v.string(), v.transform(Number)), 1),\n        },\n        v.never(),\n      ),\n    }),\n    invoice: defineRoute('/invoices/[invoiceId]', {\n      params: v.object({\n        invoiceId: v.string(),\n      }),\n    }),\n  }));\n```\n\n### Using ArkType\n\n```ts\n// src/shared/navigation.ts\nimport { createNavigationConfig } from 'next-safe-navigation';\nimport { type } from 'arktype';\n\nexport const { routes, useSafeParams, useSafeSearchParams } =\n  createNavigationConfig((defineRoute) =\u003e ({\n    customers: defineRoute('/customers', {\n      search: type({\n        'query?': \"string = ''\",\n        'page?': 'string.numeric.parse = 1',\n      }),\n    }),\n    invoice: defineRoute('/invoices/[invoiceId]', {\n      params: type({\n        invoiceId: 'string',\n      }),\n    }),\n  }));\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukemorales%2Fnext-safe-navigation","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flukemorales%2Fnext-safe-navigation","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flukemorales%2Fnext-safe-navigation/lists"}