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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@marianmeres/simple-router

v3.1.0

Published

[![NPM version](https://img.shields.io/npm/v/@marianmeres/simple-router)](https://www.npmjs.com/package/@marianmeres/simple-router) [![JSR version](https://jsr.io/badges/@marianmeres/simple-router)](https://jsr.io/@marianmeres/simple-router)

Readme

@marianmeres/simple-router

NPM version JSR version

A lightweight, framework-agnostic string pattern matcher and router with support for dynamic parameters, wildcards, query strings, and reactive subscriptions.

Can match any string identifiers - URLs, file paths, command names, or custom patterns. Originally inspired by Sapper-like regex routes. Primarily designed for client-side SPA routing, but flexible enough for any pattern matching needs.

Features

  • ✨ Dynamic route parameters with optional regex constraints
  • 🎯 Wildcard and catch-all routes
  • 📦 Spread parameters for multi-segment matching
  • 🔍 Query string parsing (with option to disable per route)
  • 🔄 Reactive subscriptions (Svelte store contract compatible)
  • 🪶 Zero dependencies (except @marianmeres/pubsub for subscriptions)
  • 📘 Full TypeScript support
  • 🎨 Framework-agnostic

Installation

deno add "jsr:@marianmeres/simple-router"
npm install @marianmeres/simple-router
import { SimpleRouter } from "@marianmeres/simple-router";

Quick Example

import { SimpleRouter } from "@marianmeres/simple-router";

// Routes can be defined via constructor config
const router = new SimpleRouter({
	"/": () => HomePage,
	"/about": () => AboutPage,
	"*": () => NotFoundPage, // catch-all fallback
});

// Or via the "on" API
router.on("/user/[id([0-9]+)]", (params) => {
	console.log("User ID:", params?.id);
	return UserPage(params?.id);
});

router.on("/article/[id]/[slug]", ({ id, slug }) => {
	return ArticlePage(id, slug);
});

// Execute route matching
const component = router.exec("/user/123");

// Use with hash routing
window.onhashchange = () => {
	const component = router.exec(location.hash.slice(1));
	render(component);
};

Route Patterns

Basic Segments

  • exact - Matches exactly "exact"
  • [name] - Matches any segment, captured as { name: "value" }
  • [name(regex)] - Matches if regex test passes
  • [name]? - Optional segment
  • [...name] - Spread params (matches multiple segments)
  • * - Wildcard (matches zero or more segments)

Separators

The default separator is /. Multiple separators are normalized to single, and separators are trimmed from both ends before matching.

Pattern Examples

| Route Pattern | URL Input | Params Result | | ----------------------------- | ------------------ | -------------------------------- | | /foo | /bar | null (no match) | | /foo | (empty) | null | | /foo/[bar] | /foo | null | | / | (empty) | {} | | foo | foo | {} | | //foo///bar.baz/ | foo/bar.baz | {} | | /[foo] | /bar | { foo: "bar" } | | #/[foo]/[bar] | #/baz/bat | { foo: "baz", bar: "bat" } | | /[id([0-9]+)] | /123 | { id: "123" } | | /[id([0-9]+)] | /foo | null (regex fails) | | /foo/[bar]/[id([0-9]+)] | /foo/baz/123 | { bar: "baz", id: "123" } | | /foo/[bar]? | /foo | {} | | /foo/[bar]? | /foo/bar | { bar: "bar" } | | /foo/[bar]?/baz | /foo/bar/baz | { bar: "bar" } | | /[...path]/[file] | /foo/bar/baz.js | { path: "foo/bar", file: "baz.js" } | | /foo/* | /foo/bar/baz.js | {} | | /[foo]/* | /foo/bar | { foo: "foo" } |

API Reference

For the complete API documentation with all methods, types, and detailed examples, see API.md.

SimpleRouter

Constructor

// Simple config (backwards compatible)
const router = new SimpleRouter({
	"/": () => HomePage,
	"/about": () => AboutPage
});

// With options object (for logger support)
const router = new SimpleRouter({
	routes: {
		"/": () => HomePage,
		"/about": () => AboutPage
	},
	logger: myLogger // optional, compatible with @marianmeres/clog
});
  • config - Either a RouterConfig object mapping route patterns to callbacks, or a RouterOptions object with routes and logger properties

Methods

on(routes, callback, options?)

Register one or more route patterns with a callback.

router.on("/users", () => UsersPage);

// Multiple routes to same handler
router.on(["/", "/home", "/index.html"], () => HomePage);

// With dynamic params
router.on("/user/[id]", (params) => UserPage(params?.id));

// With regex constraint
router.on("/post/[id([0-9]+)]", (params) => PostPage(params?.id));

// With label for debugging
router.on("/admin", () => AdminPage, { label: "admin-dashboard" });

// Disable query param parsing for this route
router.on("/raw", (params) => RawPage, { allowQueryParams: false });

Options:

  • label - Optional label for debugging (visible via info())
  • allowQueryParams - Whether to parse query parameters (default: true)

Important: Routes are matched in registration order. First match wins!

exec(url, fallbackFn?)

Execute route matching against a URL.

const result = router.exec("/users");

// With fallback
router.exec("/unknown", () => console.log("Not found"));

// With query params
router.exec("/search?q=hello");

Returns the value returned by the matched callback, or false if no match.

subscribe(callback)

Subscribe to router state changes. Follows the Svelte store contract.

const unsubscribe = router.subscribe((state) => {
	console.log("Route:", state.route);
	console.log("Params:", state.params);
	console.log("Label:", state.label);
});

// Later
unsubscribe();

Returns an unsubscribe function directly. The callback is called immediately with the current state, then on every route change.

reset()

Clears all registered routes (except catch-all).

router.reset().on("/new-route", () => NewPage);
info()

Returns a map of registered routes to their labels (for debugging).

router.on("/users", () => {}, { label: "users-list" });
console.log(router.info()); // { "/users": "users-list" }

Properties

current

Gets the current router state (readonly).

router.exec("/user/123");
console.log(router.current);
// { route: "/user/[id]", params: { id: "123" }, label: null }
static debug

Enable/disable debug logging. When enabled, uses the logger instance (if provided) or falls back to console.log.

SimpleRouter.debug = true;
router.exec("/test"); // Logs matching details to console (or custom logger)

SimpleRoute

Low-level route parser. Usually you don't need to use this directly.

import { SimpleRoute } from "@marianmeres/simple-router";

const route = new SimpleRoute("/user/[id([0-9]+)]");
const params = route.parse("/user/123");
console.log(params); // { id: "123" }

Static Methods

parseQueryString(str)

Parse a query string into an object.

SimpleRoute.parseQueryString("foo=bar&baz=123");
// Returns: { foo: "bar", baz: "123" }

TypeScript

Full TypeScript support with generic types for type-safe route callbacks and exec() return values.

Generic Router

SimpleRouter<T> is generic over T, the return type of route callbacks:

// Typed router - all callbacks must return Component, exec() returns Component | false
const router = new SimpleRouter<Component>({
	"/": () => HomePage,
	"/about": () => AboutPage,
	"*": () => NotFoundPage,
});

const result = router.exec("/about"); // Component | false
if (result !== false) {
	render(result); // result is Component
}

// Without explicit type - T is inferred from callbacks (or defaults to unknown)
const router2 = new SimpleRouter({
	"/": () => "home",
	"/about": () => "about",
});

Exported Types

import type {
	Logger,
	RouteParams,
	RouteCallback,     // RouteCallback<T = unknown>
	RouterConfig,      // RouterConfig<T = unknown>
	RouterCurrent,
	RouterOnOptions,
	RouterOptions,     // RouterOptions<T = unknown>
	RouterSubscriber,
	RouterUnsubscribe,
} from "@marianmeres/simple-router";

Advanced Examples

SPA with Hash Routing

// Type-safe router with Component return type
const router = new SimpleRouter<Component>({
	"/": () => HomePage,
	"/about": () => AboutPage,
	"/user/[id]": (params) => UserPage(params?.id),
	"*": () => NotFoundPage,
});

function render(component: Component) {
	document.getElementById("app").innerHTML = component.render();
}

window.addEventListener("hashchange", () => {
	const path = location.hash.slice(1) || "/";
	const component = router.exec(path); // Component | false
	if (component !== false) {
		render(component);
	}
});

// Trigger initial render
window.dispatchEvent(new HashChangeEvent("hashchange"));

Route Priority

Routes are matched in registration order (first match wins):

const router = new SimpleRouter();

// Register generic route first
router.on("/user/[id]", (params) => {
	console.log("Generic:", params?.id); // This will match
});

// Specific route registered second (won't match "/user/admin")
router.on("/user/admin", () => {
	console.log("Admin"); // This won't be reached
});

router.exec("/user/admin"); // Logs: "Generic: admin"

To fix, register more specific routes first:

router.on("/user/admin", () => console.log("Admin"));
router.on("/user/[id]", (params) => console.log("User:", params?.id));

Beyond URLs: General Pattern Matching

The router can match any string patterns, not just URLs:

// File path routing
const fileRouter = new SimpleRouter({
	"src/[module]/[file].ts": ({ module, file }) =>
		console.log(`Module: ${module}, File: ${file}`),
	"assets/images/[...path]": ({ path }) =>
		console.log(`Image path: ${path}`),
});

fileRouter.exec("src/components/Button.ts");
// Logs: "Module: components, File: Button"

fileRouter.exec("assets/images/icons/user.png");
// Logs: "Image path: icons/user.png"

// Command routing
const cmdRouter = new SimpleRouter({
	"user:create": () => createUser(),
	"user:delete:[id([0-9]+)]": ({ id }) => deleteUser(id),
	"cache:clear:[type(redis|memcached)]?": ({ type = "all" }) =>
		clearCache(type),
});

cmdRouter.exec("user:delete:123");
cmdRouter.exec("cache:clear:redis");
cmdRouter.exec("cache:clear"); // type defaults to "all"

// Custom separator (anything that's not a special regex char works)
const dotRouter = new SimpleRouter({
	"app.settings.theme": () => "Theme settings",
	"app.settings.[section]": ({ section }) => `Settings: ${section}`,
});

dotRouter.exec("app.settings.profile");
// Returns: "Settings: profile"