
Security News
Django Joins curl in Pushing Back on AI Slop Security Reports
Django has updated its security policies to reject AI-generated vulnerability reports that include fabricated or unverifiable content.
next-safe-navigation
Advanced tools
Type-safe navigation for NextJS App router with Standard Schema support (Zod, Valibot, ArkType, etc.)
Static type and runtime validation for navigating routes in NextJS App Router with Standard Schema support (Zod, Valibot, ArkType, etc.).
Static and runtime validation of routes, route params and query string parameters on client and server components.
Safe NextJS Navigation is available as a package on NPM, install with your favorite package manager:
npm install next-safe-navigation
You'll also need to install a Standard Schema compatible validation library:
# Choose one:
npm install zod # Zod (most popular)
npm install valibot # Valibot (lightweight)
npm install arktype # ArkType (fast)
[!TIP] Enable
experimental.typedRoutes
innext.config.js
for a better and safer experience with autocomplete when defining your routes
// src/shared/navigation.ts
import { createNavigationConfig } from 'next-safe-navigation';
import { z } from 'zod';
export const { routes, useSafeParams, useSafeSearchParams } =
createNavigationConfig((defineRoute) => ({
home: defineRoute('/'),
customers: defineRoute('/customers', {
search: z
.object({
query: z.string().default(''),
page: z.coerce.number().default(1),
})
.default({ query: '', page: 1 }),
}),
invoice: defineRoute('/invoices/[invoiceId]', {
params: z.object({
invoiceId: z.string(),
}),
}),
shop: defineRoute('/support/[...tickets]', {
params: z.object({
tickets: z.array(z.string()),
}),
}),
shop: defineRoute('/shop/[[...slug]]', {
params: z.object({
// ⚠️ Remember to always set your optional catch-all segments
// as optional values, or add a default value to them
slug: z.array(z.string()).optional(),
}),
}),
}));
[!IMPORTANT] 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 inferparams
orsearchParams
from your declared routes to be used in page props:
interface CustomersPageProps { // ❌ Do not declare your params | searchParam types searchParams?: ReturnType<typeof routes.customers.$parseSearchParams>; }
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.
// src/app/customers/page.tsx
import { routes } from "@/shared/navigation";
interface CustomersPageProps {
// ✅ Never assume the types of your params before validation
searchParams?: unknown
}
export default async function CustomersPage({ searchParams }: CustomersPageProps) {
const { query, page } = routes.customers.$parseSearchParams(searchParams);
const customers = await fetchCustomers({ query, page });
return (
<main>
<input name="query" type="search" defaultValue={query} />
<Customers data={customers} />
</main>
)
};
/* --------------------------------- */
// src/app/invoices/[invoiceId]/page.tsx
import { routes } from "@/shared/navigation";
interface InvoicePageProps {
// ✅ Never assume the types of your params before validation
params?: unknown
}
export default async function InvoicePage({ params }: InvoicePageProps) {
const { invoiceId } = routes.invoice.$parseParams(params);
const invoice = await fetchInvoice(invoiceId);
return (
<main>
<Invoice data={customers} />
</main>
)
};
// src/app/customers/page.tsx
'use client';
import { useSafeSearchParams } from "@/shared/navigation";
export default function CustomersPage() {
const { query, page } = useSafeSearchParams('customers');
const customers = useSuspenseQuery({
queryKey: ['customers', { query, page }],
queryFn: () => fetchCustomers({ query, page}),
});
return (
<main>
<input name="query" type="search" defaultValue={query} />
<Customers data={customers.data} />
</main>
)
};
/* --------------------------------- */
// src/app/invoices/[invoiceId]/page.tsx
'use client';
import { useSafeParams } from "@/shared/navigation";
export default function InvoicePage() {
const { invoiceId } = useSafeParams('invoice');
const invoice = useSuspenseQuery({
queryKey: ['invoices', { invoiceId }],
queryFn: () => fetchInvoice(invoiceId),
});
return (
<main>
<Invoice data={invoice.data} />
</main>
)
};
Use throughout your codebase as the single source for navigating between routes:
import { routes } from "@/shared/navigation";
export function Header() {
return (
<nav>
<Link href={routes.home()}>Home</Link>
<Link href={routes.customers()}>Customers</Link>
</nav>
)
};
export function CustomerInvoices({ invoices }) {
return (
<ul>
{invoices.map(invoice => (
<li key={invoice.id}>
<Link href={routes.invoice({ invoiceId: invoice.id })}>
View invoice
</Link>
</li>
))}
</ul>
)
};
This library now supports Standard Schema, which means you can use any compatible validation library:
// src/shared/navigation.ts
import { createNavigationConfig } from 'next-safe-navigation';
import { z } from 'zod';
export const { routes, useSafeParams, useSafeSearchParams } =
createNavigationConfig((defineRoute) => ({
customers: defineRoute('/customers', {
search: z
.object({
query: z.string().default(''),
page: z.coerce.number().default(1),
})
.default({ query: '', page: 1 }),
}),
invoice: defineRoute('/invoices/[invoiceId]', {
params: z.object({
invoiceId: z.string(),
}),
}),
}));
// src/shared/navigation.ts
import { createNavigationConfig } from 'next-safe-navigation';
import * as v from 'valibot';
export const { routes, useSafeParams, useSafeSearchParams } =
createNavigationConfig((defineRoute) => ({
customers: defineRoute('/customers', {
search: v.objectWithRest(
{
query: v.optional(v.string(), ''),
page: v.optional(v.pipe(v.string(), v.transform(Number)), 1),
},
v.never(),
),
}),
invoice: defineRoute('/invoices/[invoiceId]', {
params: v.object({
invoiceId: v.string(),
}),
}),
}));
// src/shared/navigation.ts
import { createNavigationConfig } from 'next-safe-navigation';
import { type } from 'arktype';
export const { routes, useSafeParams, useSafeSearchParams } =
createNavigationConfig((defineRoute) => ({
customers: defineRoute('/customers', {
search: type({
'query?': "string = ''",
'page?': 'string.numeric.parse = 1',
}),
}),
invoice: defineRoute('/invoices/[invoiceId]', {
params: type({
invoiceId: 'string',
}),
}),
}));
0.4.0
#29 cb2c4be
Thanks @mikerudge! - ### Standard Schema Support
This release introduces support for Standard Schema, allowing you to use your favorite validation library for defining route params and search query schemas.
Previously, next-safe-navigation
was tightly coupled with zod
. Now, you can use any standard-schema
compatible library, such as valibot
, arktype
, and others, while zod
continues to be supported out-of-the-box.
For existing users, your zod
schemas will continue to work without any changes.
For new users or those wishing to switch, you can now use other libraries. For example, using valibot
:
// src/shared/navigation.ts
import { createNavigationConfig } from "next-safe-navigation";
import * as v from "valibot";
export const { routes, useSafeParams, useSafeSearchParams } =
createNavigationConfig((defineRoute) => ({
customers: defineRoute("/customers", {
search: v.object({
query: v.optional(v.string(), ""),
page: v.optional(v.pipe(v.string(), v.transform(Number)), 1),
}),
}),
// ...
}));
FAQs
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Django has updated its security policies to reject AI-generated vulnerability reports that include fabricated or unverifiable content.
Security News
ECMAScript 2025 introduces Iterator Helpers, Set methods, JSON modules, and more in its latest spec update approved by Ecma in June 2025.
Security News
A new Node.js homepage button linking to paid support for EOL versions has sparked a heated discussion among contributors and the wider community.