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

@eliyya/type-routes

v3.0.0

Published

Type your Nextjs routes

Readme

type-routes

Generate a type-safe runtime and TypeScript interface from your Next.js App Router directory structure.

Install

npm add type-routes

Peer dependency: next@^16.0.0 (optional — only needed for the Next.js plugin).

CLI

type-routes --help
Usage: type-routes [options]

Options:
  -i, --input <dir>        App directory (default: src/app)
  -o, --output <file>      Output file   (default: src/lib/routes.ts)
  -w, --watch              Watch for changes
      --debounce-ms <ms>   Debounce delay (default: 300)
  -e, --extra <route>      Extra route path (can be repeated)
  -h, --help               Show this help

Examples

# Basic
type-routes

# Custom paths
type-routes -i src/app -o src/lib/routes.ts

# Watch mode
type-routes -w

# Force routes that don't exist on disk
type-routes -e / -e /users

Next.js Plugin

// next.config.ts
import { withTypeRoutes } from 'type-routes/next'

export default withTypeRoutes({
    input: 'src/app',
    output: 'src/lib/routes.ts',
    extraRoutes: ['/', '/users'],
    paramConstraints: { locale: ['en', 'es'] },
})

PluginOptions

| Option | Type | Default | Description | | ------------------ | -------------------------- | --------------------- | ------------------------------------- | | input | string | 'src/app' | App directory | | output | string | 'src/lib/routes.ts' | Output file | | extraRoutes | string[] | — | Force routes that don't exist on disk | | paramConstraints | Record<string, string[]> | — | Restrict dynamic param values | | watchDebounceMs | number | 300 | Debounce delay in dev mode |

In dev mode the plugin watches the input directory for page.tsx/route.ts changes and regenerates automatically.

Usage

Once generated, import the app object from your output file:

import { app } from '@/lib/routes' // adjust path to your output

// Static routes
app.dashboard.settings() // → '/dashboard/settings'

// Dynamic routes
app.users.$id('123') // → '/users/123'

// Catch-all routes
app.api.auth.$$all('login', 'callback') // → '/api/auth/login/callback'

// Optional catch-all routes
app.posts._$$slug() // → '/posts'
app.posts._$$slug('hello') // → '/posts/hello'

// With constraints
app.$locale('en') // allowed, → '/en'
app.$locale('fr') // type error if constraint is ['en','es']

// Nested Object.assign pattern
app.$locale.dashboard.reports.cc.$cc_id.$month.$year(
    'en',   // $locale
    'cc-1', // $cc_id
    'jan',  // $month
    '2026', // $year
) // → '/en/dashboard/reports/cc/cc-1/jan/2026'

Features

Static routes

app/dashboard/settings/page.tsx  →  app.dashboard.settings()

Dynamic routes ([param])

app/users/[id]/page.tsx  →  app.users.$id('123')

app/[locale]/dashboard/page.tsx  →  app.$locale('en')

Params are prefixed with $ when used as property names.

Catch-all routes ([...param])

app/api/auth/[...all]/route.ts  →  app.api.auth.$$all('login', 'callback')

Catch-all params are prefixed with $$. They receive rest arguments and join('/') them into the path.

Optional catch-all routes ([[...param]])

app/posts/[[...slug]]/page.tsx  →  app.posts._$$slug()
app/posts/[[...slug]]/page.tsx  →  app.posts._$$slug('hello')

Optional catch-all params are prefixed with _$$. Defaults to empty array when omitted.

Route groups ((group))

Route-group segments like (marketing) are stripped from paths and do not appear in the generated types.

Object.assign pattern

When a directory has both a page.tsx/route.ts and sub-routes, the generated runtime uses Object.assign(fn, { children }), so the node is both callable and has sub-properties:

app/dashboard/page.tsx          →  app.dashboard()         // → '/dashboard'
app/dashboard/settings/page.tsx →  app.dashboard.settings() // → '/dashboard/settings'

extraRoutes

Force-generate routes even when the corresponding file doesn't exist on disk:

withTypeRoutes({
    extraRoutes: [
        '/', // → app/       (creates app(): `/`)
        '/users', // → app/users/ (creates app.users(): `/users`)
    ],
})

| Passed | Internal path | | -------------- | ------------------------- | | '/' | app/page.tsx | | '/users' | app/users/page.tsx | | 'users/[id]' | app/users/[id]/page.tsx |

Useful when:

  • Your root (/) is served by a proxy but has no app/page.tsx
  • A directory has no page.tsx but the router resolves to a sub-route via middleware

paramConstraints

Restrict dynamic parameter values at the type level:

withTypeRoutes({
    paramConstraints: {
        locale: ['en', 'es'],
        role: ['admin', 'user'],
    },
})

Generated output:

// interface
$locale: {
    <$locale extends 'en' | 'es'>($locale: $locale): `/${$locale}/dashboard`
}

// runtime
$locale: ($locale: 'en' | 'es') => `/${$locale}/dashboard`

Using a constrained param with an invalid value produces a type error:

app.$locale('en') // OK
app.$locale('fr') // Type error: '"fr"' is not assignable to '"en" | "es"'

Keys are normalized: locale$locale, all$$all.

Programmatic API

import {
    buildTree,
    generateInterfaceFile,
    generateRuntimeFile,
    getRoutePaths,
    extractParam,
    resetId,
    type TreeNode,
    type RouteType,
} from 'type-routes'

// Read and build the tree
const paths = await getRoutePaths('./src/app')
const tree = buildTree(paths)

// Or build with constraints
const tree = buildTree(paths, { locale: ['en', 'es'] })

// Generate files
const interfaceCode = generateInterfaceFile(tree)
const runtimeCode = generateRuntimeFile(tree)

How it works

  1. Scans the input directory for page.tsx and route.ts files
  2. Builds a tree structure with node types (static, dynamic, catch-all, etc.)
  3. Generates a TypeScript interface App with recursive call signatures using template literal types
  4. Generates a runtime export const app with arrow functions and Object.assign for mixed nodes

The output is written to a single .ts file that can be imported anywhere in your project.

Motivation

Next.js file-based routing makes it easy to add routes but hard to keep references in sync when routes change. The experimental typedRoutes only covers <Link> tags, leaving redirect(), router.push(), and API calls unchecked.

type-routes generates a fully typed route object for your entire app, catching mismatches at compile time.