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

@vielzeug/routeit

v1.0.1

Published

Lightweight, type-safe client-side routing. Build powerful SPAs with minimal code and maximum flexibility.

Readme

@vielzeug/routeit

Lightweight, type-safe client-side routing. Build powerful SPAs with minimal code and maximum flexibility.

Features

  • Type-Safe - Full TypeScript support with parameter extraction
  • Framework Agnostic - Works with React, Vue, Svelte, or vanilla JS
  • Route Parameters - Extract params from dynamic routes (/users/:id)
  • Query Parameters - Automatic query string parsing
  • Middleware System - Powerful middleware for auth, logging, and more
  • Nested Routes - Support for child routes and layouts
  • Hash & History Mode - Choose between hash-based or HTML5 History API
  • Permission Integration - Works seamlessly with @vielzeug/permit
  • Lightweight - ~3.1 KB gzipped, zero dependencies
  • Developer Experience - Intuitive API with comprehensive utilities

Installation

# pnpm
pnpm add @vielzeug/routeit

# npm
npm install @vielzeug/routeit

# yarn
yarn add @vielzeug/routeit

Quick Start

import { createRouter } from '@vielzeug/routeit';

// Create a router
const router = createRouter({
  mode: 'history', // or 'hash'
  base: '/',
});

// Register routes
router
  .get('/', ({ navigate }) => {
    console.log('Home page');
    document.getElementById('app').innerHTML = '<h1>Home</h1>';
  })
  .get('/about', () => {
    console.log('About page');
    document.getElementById('app').innerHTML = '<h1>About</h1>';
  })
  .get('/users/:id', ({ params }) => {
    console.log('User ID:', params.id);
    document.getElementById('app').innerHTML = `<h1>User ${params.id}</h1>`;
  })
  .start(); // Start listening for route changes

// Navigate programmatically
router.navigate('/users/123');

Core Concepts

Router Creation

import { createRouter } from '@vielzeug/routeit';

// Basic router with defaults
const router = createRouter();

// Router with options
const router = createRouter({
  mode: 'history', // 'history' or 'hash'
  base: '/app', // Base path for all routes
  notFound: ({ pathname }) => {
    console.log('404:', pathname);
    document.getElementById('app').innerHTML = '<h1>404 Not Found</h1>';
  },
  middleware: async (ctx, next) => {
    // Global middleware - runs for every route
    console.log('Navigating to:', ctx.pathname);
    await next();
  },
});

Route Registration

// Single route
router.route({
  path: '/products/:id',
  handler: ({ params, query }) => {
    console.log('Product:', params.id);
    console.log('Search:', query.q);
  },
});

// Multiple routes at once
router.routes([
  { path: '/', handler: homeHandler },
  { path: '/about', handler: aboutHandler },
  { path: '/contact', handler: contactHandler },
]);

// Convenience method for GET-like routes
router.get('/blog', ({ pathname }) => {
  console.log('Blog page:', pathname);
});

// Method chaining
router
  .get('/', homeHandler)
  .get('/about', aboutHandler)
  .get('/users/:id', userHandler)
  .start();

Route Parameters

// Dynamic segments
router.get('/users/:userId', ({ params }) => {
  console.log('User ID:', params.userId);
  // GET /users/123 → params.userId = '123'
});

// Multiple parameters
router.get('/users/:userId/posts/:postId', ({ params }) => {
  console.log('User:', params.userId);
  console.log('Post:', params.postId);
  // GET /users/123/posts/456
  // → params.userId = '123', params.postId = '456'
});

// Wildcard routes
router.get('/docs/*', ({ pathname }) => {
  console.log('Docs path:', pathname);
  // GET /docs/guide/intro → matches
  // GET /docs/api/reference → matches
});

Query Parameters

router.get('/search', ({ query }) => {
  console.log('Query:', query.q);
  console.log('Page:', query.page);
  // GET /search?q=test&page=2
  // → query.q = 'test', query.page = '2'
});

// Array query parameters
router.get('/filter', ({ query }) => {
  console.log('Tags:', query.tags);
  // GET /filter?tags=a&tags=b&tags=c
  // → query.tags = ['a', 'b', 'c']
});

// Mixed parameters
router.get('/products/:category', ({ params, query }) => {
  console.log('Category:', params.category);
  console.log('Sort:', query.sort);
  console.log('Filters:', query.filter);
  // GET /products/electronics?sort=price&filter=new&filter=sale
  // → params.category = 'electronics'
  // → query.sort = 'price'
  // → query.filter = ['new', 'sale']
});

Navigation

// Navigate to path
router.navigate('/about');

// Navigate with query parameters
router.navigate('/search?q=test');

// Replace current entry (doesn't create history entry)
router.navigate('/login', { replace: true });

// Navigate with state
router.navigate('/profile', {
  state: { from: '/settings' },
});

// Build URLs programmatically
const url = router.buildUrl('/users/:id', { id: '123' });
console.log(url); // '/users/123'

const searchUrl = router.buildUrl('/search', undefined, {
  q: 'test',
  page: '2',
});
console.log(searchUrl); // '/search?q=test&page=2'

// Navigate from within route handler
router.get('/old-page', ({ navigate }) => {
  navigate('/new-page');
});

// History navigation
router.back(); // Go back one page
router.forward(); // Go forward one page
router.go(-2); // Go back 2 pages
router.go(1); // Go forward 1 page

Route Context

Every route handler receives a context object:

router.get('/users/:id', (context) => {
  // Route parameters
  console.log(context.params.id);

  // Query parameters
  console.log(context.query);

  // Full pathname
  console.log(context.pathname);

  // Hash (without #)
  console.log(context.hash);

  // Custom data (if provided in route definition)
  console.log(context.data);

  // Navigate function
  context.navigate('/another-page');
});

// TypeScript: Type the context
type RouteData = { requiresAuth: boolean };

router.route<RouteData>({
  path: '/admin',
  handler: (context) => {
    // context.data is typed as RouteData
    if (context.data?.requiresAuth) {
      console.log('Auth required');
    }
  },
  data: { requiresAuth: true },
});

Middleware

Middleware allows you to execute code before route handlers, modify context, or block navigation.

import type { Middleware } from '@vielzeug/routeit';

// Basic middleware
const loggerMiddleware: Middleware = async (ctx, next) => {
  console.log('Navigating to:', ctx.pathname);
  await next(); // Continue to next middleware or handler
  console.log('Navigation complete');
};

// Authentication middleware
const requireAuth: Middleware = async (ctx, next) => {
  const user = await getCurrentUser();
  
  if (!user) {
    ctx.navigate('/login');
    return; // Don't call next() - blocks execution
  }
  
  ctx.user = user; // Add user to context
  await next();
};

// Route-specific middleware
router.route({
  path: '/dashboard',
  middleware: requireAuth,
  handler: (ctx) => {
    console.log('User:', ctx.user);
  }
});

// Multiple middleware (executed in order)
router.route({
  path: '/admin',
  middleware: [requireAuth, requireAdmin],
  handler: () => {
    console.log('Admin page');
  }
});

// Global middleware (runs for all routes)
const router = createRouter({
  middleware: [loggerMiddleware, errorHandler]
});

Middleware Execution Order:

Global Middleware 1
  ↓
Global Middleware 2
  ↓
Route Middleware 1
  ↓
Route Middleware 2
  ↓
Route Handler

Context Enhancement:

// Middleware can modify the context
const dataLoader: Middleware = async (ctx, next) => {
  // Add metadata
  ctx.meta = { 
    loadedAt: Date.now(),
    environment: 'production' 
  };
  
  // Load user data
  ctx.user = await fetchUser();
  
  await next();
};

// Handler can access enhanced context
router.route({
  path: '/profile',
  middleware: dataLoader,
  handler: (ctx) => {
    console.log('User:', ctx.user);
    console.log('Meta:', ctx.meta);
  }
});

Integration with @vielzeug/permit:

import { Permit } from '@vielzeug/permit';
import type { BaseUser, PermissionAction } from '@vielzeug/permit';

// Permission middleware factory
function requirePermission(
  resource: string, 
  action: PermissionAction
): Middleware {
  return async (ctx, next) => {
    const user = ctx.user as BaseUser;
    
    if (!user || !Permit.check(user, resource, action)) {
      ctx.navigate('/forbidden');
      return;
    }
    
    await next();
  };
}

// Usage
router.route({
  path: '/posts',
  middleware: [requireAuth, requirePermission('posts', 'read')],
  handler: () => console.log('Posts page')
});

router.route({
  path: '/posts/:id/edit',
  middleware: [requireAuth, requirePermission('posts', 'update')],
  handler: ({ params }) => console.log('Edit post:', params.id)
});

Nested Routes

router.route({
  path: '/users',
  handler: () => {
    console.log('Users section');
  },
  children: [
    {
      path: '/list',
      handler: () => {
        console.log('User list');
        // GET /users/list
      },
    },
    {
      path: '/:id',
      handler: ({ params }) => {
        console.log('User details:', params.id);
        // GET /users/123
      },
    },
    {
      path: '/:id/edit',
      handler: ({ params }) => {
        console.log('Edit user:', params.id);
        // GET /users/123/edit
      },
    },
  ],
});

// Nested routes with parameters
router.route({
  path: '/organizations/:orgId',
  handler: ({ params }) => {
    console.log('Organization:', params.orgId);
  },
  children: [
    {
      path: '/projects/:projectId',
      handler: ({ params }) => {
        // Both orgId and projectId are available
        console.log('Org:', params.orgId);
        console.log('Project:', params.projectId);
        // GET /organizations/abc/projects/xyz
      },
    },
  ],
});

Subscriptions

// Subscribe to route changes
const unsubscribe = router.subscribe(() => {
  console.log('Route changed!');
  console.log('Current path:', router.getCurrentPath());
  console.log('Query params:', router.getCurrentQuery());
});

// Later... unsubscribe
unsubscribe();

// React integration
import { useEffect, useState } from 'react';

function useRouter(router) {
  const [, setTick] = useState(0);

  useEffect(() => {
    return router.subscribe(() => {
      setTick((t) => t + 1); // Force re-render
    });
  }, [router]);

  return {
    pathname: router.getCurrentPath(),
    query: router.getCurrentQuery(),
    navigate: router.navigate.bind(router),
  };
}

// Usage
function App() {
  const { pathname, query, navigate } = useRouter(router);

  return (
    <div>
      <p>Current path: {pathname}</p>
      <button onClick={() => navigate('/about')}>Go to About</button>
    </div>
  );
}

Utilities

// Check if route is active
if (router.isActive('/users/:id')) {
  console.log('On user page');
}

if (router.isActive('/admin/*')) {
  console.log('In admin section');
}

// Get current route info
const pathname = router.getCurrentPath();
const query = router.getCurrentQuery();
const hash = router.getCurrentHash();

console.log('Current:', pathname); // '/users/123'
console.log('Query:', query); // { tab: 'profile' }
console.log('Hash:', hash); // 'section-1'

// Build URLs
const userUrl = router.buildUrl('/users/:id', { id: '123' });
console.log(userUrl); // '/users/123'

const searchUrl = router.buildUrl(
  '/search',
  undefined,
  { q: 'test', filter: ['new', 'sale'] }
);
console.log(searchUrl); // '/search?q=test&filter=new&filter=sale'

const fullUrl = router.buildUrl(
  '/users/:id',
  { id: '123' },
  { tab: 'posts', page: '2' }
);
console.log(fullUrl); // '/users/123?tab=posts&page=2'

Advanced Usage

Custom 404 Handler

const router = createRouter({
  notFound: ({ pathname, navigate }) => {
    console.log('404:', pathname);
    
    // Render 404 page
    document.getElementById('app').innerHTML = `
      <div>
        <h1>404 - Page Not Found</h1>
        <p>The page "${pathname}" does not exist.</p>
        <button onclick="router.navigate('/')">Go Home</button>
      </div>
    `;
    
    // Or redirect
    // navigate('/', { replace: true });
  },
});

Route Data & Metadata

type RouteMetadata = {
  title: string;
  requiresAuth?: boolean;
  roles?: string[];
};

router.route<RouteMetadata>({
  path: '/admin',
  handler: ({ data }) => {
    document.title = data?.title || 'App';
    console.log('Required roles:', data?.roles);
  },
  data: {
    title: 'Admin Dashboard',
    requiresAuth: true,
    roles: ['admin'],
  },
  middleware: async ({ data }, next) => {
    if (data?.requiresAuth && !isAuthenticated()) {
      router.navigate('/login');
      return;
    }
    if (data?.roles && !hasRole(data.roles)) {
      router.navigate('/forbidden');
      return;
    }
    await next();
  },
});

Hash Mode

// Use hash-based routing for GitHub Pages or static hosting
const router = createRouter({
  mode: 'hash',
  base: '/',
});

router
  .get('/', () => {
    console.log('Home');
    // URL: https://example.com/#/
  })
  .get('/about', () => {
    console.log('About');
    // URL: https://example.com/#/about
  })
  .get('/users/:id', ({ params }) => {
    console.log('User:', params.id);
    // URL: https://example.com/#/users/123
  })
  .start();

Base Path

// App deployed at /my-app/ instead of root
const router = createRouter({
  mode: 'history',
  base: '/my-app',
});

router.get('/about', () => {
  console.log('About');
  // Full URL: https://example.com/my-app/about
});

router.navigate('/contact');
// Navigates to: https://example.com/my-app/contact

Bundle Size

  • Raw: ~9.9 KB
  • Minified: ~5.7 KB
  • Gzipped: ~3.1 KB (ESM) / ~2.1 KB (CJS)

TypeScript Support

Fully typed with comprehensive type definitions:

import type {
  Router,
  RouteContext,
  RouteDefinition,
  RouteHandler,
  Middleware,
  RouteParams,
  QueryParams,
  NavigateOptions,
  RouterMode,
  RouterOptions,
} from '@vielzeug/routeit';

Browser Support

Works in all modern browsers that support:

  • ES6 (ES2015)
  • URL API
  • URLSearchParams
  • History API (for history mode)
  • hashchange event (for hash mode)

License

MIT

Contributing

Contributions are welcome! Please read our contributing guidelines for details.

Links