safe-router
v0.6.0
Published
⚡️ Type-safe routing for file-based routing frameworks
Downloads
441
Maintainers
Readme
safe-router
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
- Next.js App Router
- Next.js Pages Router
- Astro 3+
- Remix 2+
- React Router 7+
Requirements
- TypeScript >=5.0
Setup
Install safe-router and make sure your editor uses your workspace TypeScript version.
npm install safe-router
npm install --save-dev typescriptsafe-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-routerwrites 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.tsxsrc/
└── app/
└── page.tsxIf 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 --watchFor 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 --watchCLI options:
--config <path>: Path totsconfig.json. Defaults to the nearesttsconfig.jsonfrom the current directory.--watch: Watch the resolved routes directory and regenerate on changes.--project-root <dir>: Project root for resolvingroutesDirandoutputFile. Defaults to the config directory.--router <name>,--src-dir <dir>,--routes-dir <dir>, and--output-file <path>: Override the matchingtsconfigplugin 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.jsonProgrammatic 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-routerRouter 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.tsxsafe-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() // -> /settingsUse 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=helloAdditional 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.
