# next-safe-navigation

> Type-safe navigation for NextJS App router with Standard Schema support (Zod, Valibot, ArkType, etc.)

Latest version **0.4.0** (published 2025-06-24) · MIT license · 0 weekly downloads

## Install

```sh
npm install next-safe-navigation
pnpm add next-safe-navigation
yarn add next-safe-navigation
bun add next-safe-navigation
```

## Health

**Score 40/100 (D)** — status: maintenance-mode.

Positive: has types; esm support; no vulnerabilities; high quality score.

Warnings: low downloads; pre 1.0.

Negative: stale; low maintenance score.

## Facts

| | |
|---|---|
| Version | 0.4.0 |
| Published | 2025-06-24 |
| First published | 2024-02-02 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 1 |
| Unpacked size | 80.7 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 175 |
| Author | Luke Morales |
| Maintainers | lukemorales |
| Keywords | next, nextjs, next.js, next-js, app router, app-router, vercel, typescript, type-safety, router, navigation, standard-schema, zod, valibot, arktype, runtime validation, validation |

## Links

- npm: https://www.npmjs.com/package/next-safe-navigation
- Repository: https://github.com/lukemorales/safe-next-navigation
- Homepage: https://github.com/lukemorales/safe-next-navigation#readme
- Issues: https://github.com/lukemorales/safe-next-navigation/issues
- npm.io page: https://npm.io/package/next-safe-navigation

## Dependencies (1)

- [@standard-schema/spec](https://npm.io/package/@standard-schema/spec.md) ^1.0.0

## Alternatives

- [express-promise-router](https://npm.io/package/express-promise-router.md) — 736.1K weekly downloads
- [next-usequerystate](https://npm.io/package/next-usequerystate.md) — 29.8K weekly downloads
- [@bitkyc08/opencodex](https://npm.io/package/@bitkyc08/opencodex.md) — 4.6K weekly downloads
- [lynkr](https://npm.io/package/lynkr.md) — 575 weekly downloads
- [baremetal.js](https://npm.io/package/baremetal.js.md) — 42 weekly downloads

## Recent versions

- 0.4.0 (latest) — 2025-06-24
- 0.3.3 — 2024-08-17
- 0.3.2 — 2024-04-22
- 0.3.1 — 2024-03-30
- 0.3.0 — 2024-03-29
- 0.2.0 — 2024-03-27
- 0.1.1 — 2024-02-04
- 0.1.0 — 2024-02-02

## README

<p align="center">
  <a href="https://github.com/lukemorales/next-safe-navigation" target="\_parent"><img src="https://em-content.zobj.net/source/apple/354/goggles_1f97d.png" alt="Goggles emoji" height="130"></a>
</p>

<h1 align="center">Safe NextJS Navigation</h1>

<p align="center">
  <a href="https://github.com/lukemorales/next-safe-navigation/actions/workflows/tests.yml" target="\_parent"><img src="https://github.com/lukemorales/next-safe-navigation/actions/workflows/tests.yml/badge.svg?branch=main" alt="Latest build"></a>
  <a href="https://codecov.io/gh/lukemorales/next-safe-navigation"><img src="https://codecov.io/gh/lukemorales/next-safe-navigation/graph/badge.svg?token=35GW5EJMFK"/></a>
  <a href="https://www.npmjs.com/package/next-safe-navigation" target="\_parent"><img src="https://badgen.net/npm/v/next-safe-navigation" alt="Latest published version"></a>
  <a href="https://bundlephobia.com/package/next-safe-navigation@latest" target="\_parent"><img src="https://badgen.net/bundlephobia/minzip/next-safe-navigation" alt="Bundlephobia"></a>
  <a href="https://bundlephobia.com/package/next-safe-navigation@latest" target="\_parent"><img src="https://badgen.net/bundlephobia/tree-shaking/next-safe-navigation" alt="Tree shaking available"></a>
  <a href="https://github.com/lukemorales/next-safe-navigation" target="\_parent"><img src="https://badgen.net/npm/types/next-safe-navigation" alt="Types included"></a>
  <a href="https://www.npmjs.com/package/next-safe-navigation" target="\_parent"><img src="https://badgen.net/npm/license/next-safe-navigation" alt="License"></a>
  <a href="https://www.npmjs.com/package/next-safe-navigation" target="\_parent"><img src="https://badgen.net/npm/dt/next-safe-navigation" alt="Number of downloads"></a>
  <a href="https://github.com/lukemorales/next-safe-navigation" target="\_parent"><img src="https://img.shields.io/github/stars/lukemorales/next-safe-navigation.svg?style=social&amp;label=Star" alt="GitHub Stars"></a>
</p>

<p align="center">
  <strong>Static type and runtime validation for navigating routes in <a href="https://nextjs.org" target="\_parent">NextJS App Router</a> with Standard Schema support (Zod, Valibot, ArkType, etc.).</strong>
</p>

<p align="center">
  Static and runtime validation of routes, route params and query string parameters on client and server components.
</p>

## 📦 Install

Safe NextJS Navigation is available as a package on NPM, install with your favorite package manager:

```dircolors
npm install next-safe-navigation
```

You'll also need to install a Standard Schema compatible validation library:

```dircolors
# Choose one:
npm install zod           # Zod (most popular)
npm install valibot       # Valibot (lightweight)
npm install arktype       # ArkType (fast)
```

## ⚡ Quick start

> [!TIP]
> Enable `experimental.typedRoutes` in `next.config.js` for a better and safer experience with autocomplete when defining your routes

### Declare your application routes and parameters in a single place

```ts
// 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(),
      }),
    }),
  }));
```

### Runtime validation for React Server Components (RSC)

> [!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 infer `params` or `searchParams` from your declared routes to be used in page props:
>
> ```ts
> 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.

```ts
// 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>
  )
};
```

### Runtime validation for Client Components

```ts
// 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:

```ts
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>
  )
};
```

## 🔄 Standard Schema Support

This library now supports [Standard Schema](https://github.com/standard-schema/standard-schema), which means you can use any compatible validation library:

### Using Zod

```ts
// 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(),
      }),
    }),
  }));
```

### Using Valibot

```ts
// 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(),
      }),
    }),
  }));
```

### Using ArkType

```ts
// 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',
      }),
    }),
  }));
```

---
_Source: https://npm.io/package/next-safe-navigation · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
