Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/lukemorales/next-safe-navigation

Static type and runtime validation for navigating routes in NextJS App Router with Zod schemas
https://github.com/lukemorales/next-safe-navigation

app-router app-router-nextjs navigation next next-js nextjs runtime-validation static-typechecking type-safety typescript validation vercel zod

Last synced: 7 days ago
JSON representation

Static type and runtime validation for navigating routes in NextJS App Router with Zod schemas

Awesome Lists containing this project

README

        


Goggles emoji

Safe NextJS Navigation


Latest build

Latest published version
Bundlephobia
Tree shaking available
Types included
License
Number of downloads
GitHub Stars


Static type and runtime validation for navigating routes in NextJS App Router with Zod schemas.


Static and runtime validation of routes, route params and query string parameters on client and server components.

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

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

## ⚡ 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 Zod schema might not be the same as its input, since schemas can transform the values during parsing (e.g.: `z.coerce.number()`), 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
> }
>```
> 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 (



)
};

/* --------------------------------- */

// 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 (



)
};
```

### 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 (



)
};

/* --------------------------------- */

// 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 (



)
};
```

Use throughout your codebase as the single source for navigating between routes:

```ts
import { routes } from "@/shared/navigation";

export function Header() {
return (

Home
Customers

)
};

export function CustomerInvoices({ invoices }) {
return (


    {invoices.map(invoice => (


  • View invoice


  • ))}

)
};
```