# safe-router

[![NPM Version][npm-image]][npm-url]
[![Github License][license-image]](LICENSE)
[![NPM Downloads][downloads-image]][npm-url]
[![Codecov][codecov-image]][codecov-url]

Automagic type-safe route generation for file-based routing frameworks.

## Why?

Maintaining URLs by hand is tedious and easy to get wrong. `safe-router` reads your route files and generates a typed `routes` object that you can use throughout your app.

Instead of writing a dynamic URL string by hand:

```tsx
<Link href="/products/[id]" />
```

you get autocomplete and route parameter types:

```tsx
<Link href={routes.products.id('123').get()} />
```

## Supported Frameworks

- [Next.js](https://nextjs.org/) App Router
- [Next.js](https://nextjs.org/) Pages Router
- [Astro 3+](https://astro.build/)
- [Remix 2+](https://remix.run/)
- [React Router 7+](https://reactrouter.com/)

## Requirements

- [TypeScript](https://www.typescriptlang.org/) >=5.0

## Setup

Install `safe-router` and make sure your editor uses your workspace TypeScript version.

```bash
npm install safe-router
npm install --save-dev typescript
```

`safe-router/helpers` is published as compiled JavaScript with TypeScript declarations, so app bundlers do not need to transpile `safe-router` just to consume the generated route helpers.

In VS Code, add this to `.vscode/settings.json`:

```jsonc
{
  "typescript.tsdk": "node_modules/typescript/lib"
}
```

Then add the TypeScript plugin to `tsconfig.json`.

```jsonc
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "safe-router",
        "router": "nextjs-app"
      }
    ]
  }
}
```

## Configuration

`safe-router` can be configured through the TypeScript plugin entry in `tsconfig.json`. The editor plugin and the CLI both read these options, so a single config can serve IDE, CI, and agent workflows.

```jsonc
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "safe-router",
        "router": "nextjs-app",
        "srcDir": "src",
        "routesDir": "src/app",
        "outputFile": "src/routes.generated.ts"
      }
    ]
  }
}
```

Options:

- `router` *(optional)*: Router adapter to use. Supported values are `"nextjs-app"`, `"nextjs-pages"`, `"astro"`, `"remix"`, and `"react-router"`. Defaults to `"nextjs-app"`.
- `srcDir` *(optional)*: Source directory to use when your project keeps application code under a folder such as `"src"`. This narrows route autodetection to that source directory.
- `routesDir` *(optional)*: Explicit route directory, relative to the project root. Use this when your project does not follow the adapter's default route locations.
- `outputFile` *(optional)*: Generated routes file, relative to the project root. When omitted, `safe-router` writes the file near the application source root.

Default route detection:

| Router | Default route directories | Default output |
| --- | --- | --- |
| `nextjs-app` | `app`, `src/app` | `routes.generated.ts`, or `src/routes.generated.ts` when `src/app` is detected |
| `nextjs-pages` | `pages`, `src/pages` | `routes.generated.ts`, or `src/routes.generated.ts` when `src/pages` is detected |
| `astro` | `src/pages` | `src/routes.generated.ts` |
| `remix` | `app/routes` | `app/routes.generated.ts` |
| `react-router` | `app/routes` | `app/routes.generated.ts` |

Supported route conventions:

| Router | Supported conventions |
| --- | --- |
| `nextjs-app` | `page` files, `route` files, static segments, `[param]`, `[...param]`, `[[...param]]`, route groups, private folders, parallel route folders, and intercepted route prefixes |
| `nextjs-pages` | `index` files, direct page files such as `about.tsx`, `[param]`, `[...param]`, and `[[...param]]` |
| `astro` | `.astro`, `.md`, `.mdx`, `.js`, and `.ts` route files, `index` files, `[param]`, and rest parameters such as `[...path]` |
| `remix` | Remix v2 flat routes: dot delimiters, `_index`, `$param`, `$` splats, optional segments, pathless `_` layout segments, and folder `route` files |
| `react-router` | React Router file routes: dot delimiters, `_index`, `$param`, `$` splats, optional segments, pathless `_` layout segments, and folder `route` files |

For example, both of these Next.js App Router layouts work without extra options:

```txt
app/
└── page.tsx
```

```txt
src/
└── app/
    └── page.tsx
```

If both layouts exist or you want to be explicit, set `srcDir` or `routesDir`:

```jsonc
{
  "compilerOptions": {
    "plugins": [
      {
        "name": "safe-router",
        "router": "nextjs-app",
        "srcDir": "src"
      }
    ]
  }
}
```

## CLI / CI / Agent Workflows

Use the CLI when you need deterministic route generation outside an IDE or `tsserver`, such as CI, build scripts, and agentic coding workflows.

```bash
npx safe-router generate
npx safe-router generate --config tsconfig.json
npx safe-router generate --watch
```

For a Next.js App Router project that uses `src/app`, keep the plugin config in `tsconfig.json` and run the CLI from the project root:

```jsonc
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "plugins": [
      {
        "name": "safe-router",
        "router": "nextjs-app",
        "srcDir": "src",
        "routesDir": "src/app",
        "outputFile": "src/routes.generated.ts"
      }
    ]
  }
}
```

Then wire generation before commands that need the generated file:

```jsonc
{
  "scripts": {
    "routes:generate": "safe-router generate",
    "dev": "npm run routes:generate && next dev",
    "build": "npm run routes:generate && next build",
    "typecheck": "npm run routes:generate && tsc --noEmit"
  }
}
```

During local development you can keep the generator running:

```bash
safe-router generate --watch
```

CLI options:

- `--config <path>`: Path to `tsconfig.json`. Defaults to the nearest `tsconfig.json` from the current directory.
- `--watch`: Watch the resolved routes directory and regenerate on changes.
- `--project-root <dir>`: Project root for resolving `routesDir` and `outputFile`. Defaults to the config directory.
- `--router <name>`, `--src-dir <dir>`, `--routes-dir <dir>`, and `--output-file <path>`: Override the matching `tsconfig` plugin option.
- `--format biome|prettier|false`: Run the project's local formatter after writing the file.

In monorepos, run the command from each app package or pass that package's config explicitly:

```bash
safe-router generate --config apps/web/tsconfig.json
```

Programmatic generation is also available:

```ts
import { generateRoutes, watchRoutes } from 'safe-router/generate'

generateRoutes({
  tsconfigPath: 'tsconfig.json',
})

const watcher = watchRoutes({
  tsconfigPath: 'tsconfig.json',
})

watcher.close()
```

This repository also ships a skills.sh-compatible agent skill in `skills/safe-router/`. Install it in agent harnesses that support skills.sh with:

```bash
npx skills add ivanfilhoz/safe-router
```

## Router Examples

Next.js App Router:

```jsonc
{
  "compilerOptions": {
    "plugins": [{ "name": "safe-router", "router": "nextjs-app" }]
  }
}
```

Next.js Pages Router:

```jsonc
{
  "compilerOptions": {
    "plugins": [{ "name": "safe-router", "router": "nextjs-pages" }]
  }
}
```

Astro:

```jsonc
{
  "compilerOptions": {
    "plugins": [{ "name": "safe-router", "router": "astro" }]
  }
}
```

Remix:

```jsonc
{
  "compilerOptions": {
    "plugins": [{ "name": "safe-router", "router": "remix" }]
  }
}
```

React Router:

```jsonc
{
  "compilerOptions": {
    "plugins": [{ "name": "safe-router", "router": "react-router" }]
  }
}
```

## Usage

Given this Next.js App Router structure:

```txt
app/
├── api/
│   ├── [[...apiRoute]]/
│   │   └── route.ts
│   └── route.ts
├── products/
│   ├── [id]/
│   │   ├── details/
│   │   │   └── page.tsx
│   │   └── page.tsx
│   └── page.tsx
├── settings/
│   └── page.tsx
└── page.tsx
```

`safe-router` generates a `routes` object:

```ts
import { routes } from '@/routes.generated'

routes.get() // -> /
routes.api.get() // -> /api
routes.api.apiRoute('hello', 'world').get() // -> /api/hello/world
routes.products.get() // -> /products
routes.products.id('123').get() // -> /products/123
routes.products.id('123').details.get() // -> /products/123/details
routes.settings.get() // -> /settings
```

Use the generated `RouteParams` type for route params:

```tsx
import type { RouteParams } from '@/routes.generated'

type Props = {
  params: RouteParams['products.id.details']
}

export default function ProductDetailsPage({ params }: Props) {
  return <div>Details for product {params.id}</div>
}
```

## Search Parameters

Search parameters are supported by using the generated `CreateSearchParams` re-export in a route file:

```tsx
import type { CreateSearchParams, RouteParams } from '@/routes.generated'

export type Props = {
  params: RouteParams['products.id.details']
  searchParams: CreateSearchParams<{ tab: string }>
}

export default function ProductDetailsPage({ params, searchParams }: Props) {
  const currentTab = searchParams.tab ?? 'default'

  return (
    <div>
      Details for product {params.id}
      Current tab: {currentTab}
    </div>
  )
}
```

The typed search params become the typed argument to `get`:

```ts
routes.products.id('123').details.get({
  tab: 'specs',
  otherParam: 'hello',
})

// -> /products/123/details?tab=specs&otherParam=hello
```

Additional search params are still accepted so you can pass through values that are not part of the route file's declared search param type.

You may also import `CreateSearchParams` from a local wrapper, as long as the wrapper re-exports or aliases the generated/helper type:

```ts
// src/routes.ts
export { routes } from './routes.generated'
export type { CreateSearchParams, RouteParams } from './routes.generated'
```

```tsx
import type { CreateSearchParams } from '@/routes'
```

The generator follows resolvable imports, re-exports, and simple type aliases. A structurally similar custom type that is not connected to `CreateSearchParams` is intentionally ignored.

## License

[MIT](LICENSE)

[npm-image]: https://img.shields.io/npm/v/safe-router.svg
[license-image]: https://img.shields.io/github/license/ivanfilhoz/safe-router.svg
[downloads-image]: https://img.shields.io/npm/dm/safe-router.svg
[npm-url]: https://npmjs.org/package/safe-router
[codecov-image]: https://codecov.io/gh/ivanfilhoz/safe-router/branch/main/graph/badge.svg?token=A1ANWBUXNO
[codecov-url]: https://codecov.io/gh/ivanfilhoz/safe-router
