npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

safe-router

v0.6.0

Published

⚡️ Type-safe routing for file-based routing frameworks

Downloads

441

Readme

safe-router

NPM Version Github License NPM Downloads Codecov

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:

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

you get autocomplete and route parameter types:

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

Supported Frameworks

Requirements

Setup

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

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:

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

Then add the TypeScript plugin to tsconfig.json.

{
  "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.

{
  "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:

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

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

{
  "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.

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:

{
  "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:

{
  "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:

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:

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

Programmatic generation is also available:

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:

npx skills add ivanfilhoz/safe-router

Router Examples

Next.js App Router:

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

Next.js Pages Router:

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

Astro:

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

Remix:

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

React Router:

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

Usage

Given this Next.js App Router structure:

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:

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:

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:

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:

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:

// src/routes.ts
export { routes } from './routes.generated'
export type { CreateSearchParams, RouteParams } from './routes.generated'
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