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

routes.ts

v1.0.1

Published

Zero-dependency, type-safe route management. Define routes once with :param syntax, get fully typed URL generation, matching, and route tree flattening for free.

Readme

routes.ts

Zero-dependency, type-safe route management for TypeScript. Define your routes once, get type-checked URL generation, pattern matching, and route tree utilities for free.

Works with any framework — Next.js, Express, React Router, SvelteKit, or plain TypeScript.

Install

npm install routes.ts

Quick Start

import { defineRoutes, generateRoute } from 'routes.ts';

const routes = defineRoutes({
  home: '/',
  login: '/login',
  blog: {
    list: '/blog',
    post: '/blog/posts/:id',
  },
  users: {
    profile: '/users/:userId',
    posts: '/users/:userId/posts/:postId',
  },
} as const);

// Static routes — just reference directly
routes.login               // → '/login'

// Dynamic routes — TypeScript enforces the params
generateRoute(routes.blog.post, { id: 42 })
// → '/blog/posts/42'

generateRoute(routes.users.posts, { userId: 5, postId: 99 })
// → '/users/5/posts/99'

// With query strings
generateRoute(routes.blog.post, { id: 42 }, { tab: 'comments', page: 2 })
// → '/blog/posts/42?tab=comments&page=2'

API

defineRoutes(routes)

Wraps your route definition object with as const inference and deep-freezes it at runtime. This is the single source of truth for all your routes.

const routes = defineRoutes({
  home: '/',
  settings: {
    root: '/settings',
    profile: '/settings/profile',
  },
} as const);

routes.settings.profile  // type: '/settings/profile' (string literal, not just string)
routes.home = '/x';      // ❌ TypeError — object is frozen

generateRoute(pattern, params?, query?)

Substitutes :param segments in a route pattern and optionally appends a query string. TypeScript infers which parameters are required from the pattern string.

// Single param — { id: string | number } is required
generateRoute('/blog/:id', { id: 42 })
// → '/blog/42'

// Multiple params
generateRoute('/users/:userId/posts/:postId', { userId: 5, postId: 99 })
// → '/users/5/posts/99'

// With query string
generateRoute('/blog/:id', { id: 42 }, { tab: 'comments' })
// → '/blog/42?tab=comments'

// Static route with query string
generateRoute('/search', {}, { q: 'hello', page: 1 })
// → '/search?q=hello&page=1'

// Missing params → runtime error
generateRoute('/blog/:id', {})
// ❌ Error: Missing required route parameter: "id"

Type safety in action:

// ✅ Compiles — all params present
generateRoute(routes.users.posts, { userId: 5, postId: 99 })

// ❌ TypeScript error — missing 'postId'
generateRoute(routes.users.posts, { userId: 5 })

// ❌ TypeScript error — 'oops' is not a valid param
generateRoute(routes.blog.post, { oops: 42 })

matchRoute(pattern, pathname)

Tests whether a pathname matches a route pattern and extracts parameter values. Returns an object of params on match, or null on mismatch. Query strings are stripped before matching.

matchRoute('/blog/:id', '/blog/42')
// → { id: '42' }

matchRoute('/users/:userId/posts/:postId', '/users/5/posts/99')
// → { userId: '5', postId: '99' }

matchRoute('/blog/:id', '/login')
// → null

// Query strings are ignored
matchRoute('/blog/:id', '/blog/42?tab=comments')
// → { id: '42' }

flattenRoutes(routes)

Flattens a nested route tree into a flat Record<string, string> using dot-notation keys. Useful for debugging, sitemap generation, or admin dashboards.

flattenRoutes({
  home: '/',
  blog: {
    list: '/blog',
    post: '/blog/:slug',
  },
})
// → {
//     'home': '/',
//     'blog.list': '/blog',
//     'blog.post': '/blog/:slug',
//   }

Type Exports

All types are exported for use in your own code:

import type {
  ExtractParams,   // Extracts ':param' names as a union type
  RouteParams,     // Maps param names to { [name]: string | number }
  QueryParams,     // Record<string, string | number | boolean | undefined>
  DeepReadonly,    // Recursive Readonly<T>
  RouteDefinition, // string | nested object of strings
  GenerateArgs,    // Conditional arg types for generateRoute
} from 'routes.ts';

// Example: extract params from a route pattern
type Params = ExtractParams<'/users/:userId/posts/:postId'>;
// → 'userId' | 'postId'

Framework Examples

Next.js

// lib/routes.ts
import { defineRoutes } from 'routes.ts';

export const routes = defineRoutes({
  home: '/',
  blog: { post: '/blog/:slug' },
} as const);
// components/PostLink.tsx
import Link from 'next/link';
import { routes } from '@/lib/routes';
import { generateRoute } from 'routes.ts';

export function PostLink({ slug, title }: { slug: string; title: string }) {
  return (
    <Link href={generateRoute(routes.blog.post, { slug })}>
      {title}
    </Link>
  );
}
// In router.push
const router = useRouter();
router.push(generateRoute(routes.blog.post, { slug: 'hello-world' }));

Express / Node.js

import express from 'express';
import { routes, generateRoute, matchRoute } from './routes';

const app = express();

app.get('*', (req, res) => {
  const match = matchRoute(routes.blog.post, req.path);
  if (match) {
    return res.json({ slug: match.slug });
  }
  res.status(404).json({ error: 'Not found' });
});

API Calls

const url = generateRoute(routes.api.users.byId, { id: userId });
const res = await fetch(url);

Design Decisions

Why not auto-generate from the filesystem? Filesystem-based route scanners (like nextjs-routes) are clever but fragile — they break across Next.js versions, require build plugins, and add hidden complexity. A plain TypeScript object is explicit, grepable, and works in any framework.

Why :param syntax? It's the most widely understood parameter syntax (Express, Fastify, React Router, etc.). TypeScript template literal types can extract parameter names directly from these strings, giving you full type safety with zero code generation.

Why deep freeze? Routes are constants. Accidentally mutating routes.home = '/oops' in a middleware would be a nasty bug. Freezing catches this immediately.

License

MIT