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

@the-memoize-project/router

v0.2.0

Published

Memoize Router: Declarative routing system for The Memoize Project. A lightweight, zero-dependency router for web components with reactive navigation and nested routing support.

Readme

🧭 Memoize Router

Declarative routing system for The Memoize Project. Zero dependencies, fully reactive.

Part of The Memoize Project — A modern flashcard application with FSRS spaced repetition

npm version npm downloads License: MIT Bundle Size CI Status TypeScript PRs Welcome

Getting Started · Documentation · Examples · Contributing


📚 About The Memoize Project

The Memoize Project is a modern, personal flashcard application designed for effective learning through spaced repetition. Born from a comprehensive architectural refactoring, the project embraces a micro-repository architecture where each context is independently maintained and versioned.

🎯 Project Context

  • Mission: Building a powerful flashcard application with cutting-edge spaced repetition algorithms
  • Evolution: Migrating from Anki's SM-2 algorithm to the more sophisticated FSRS (Free Spaced Repetition Scheduler)
  • Architecture: Modern micro-repo structure with independent, focused modules
  • Organization: github.com/the-memoize-project

🧩 Repository Purpose

This repository (router) provides the routing system that powers navigation in the Memoize application. It's designed to be:

  • Lightweight: Zero dependencies, minimal footprint (~2KB minified)
  • Declarative: Define routes without imperative navigation code
  • Reactive: Automatic route matching and parameter extraction
  • Pattern-Based: Supports dynamic route parameters like /users/:id
  • Multi-Platform: Works in browsers and Cloudflare Workers

The router seamlessly integrates with Web Components and handles all navigation needs for both client-side and edge computing environments.


🌟 What is Memoize Router?

Memoize Router is a lightweight, declarative routing system built for modern web applications. It provides simple but powerful routing capabilities with pattern matching, dynamic parameters, and fallback routes.

✨ Key Features

  • 🎯 Pattern Matching - Define routes with dynamic parameters (/users/:id)
  • ⚡ Zero Dependencies - Built on native Web APIs (History API, URL)
  • 🪶 Ultra Lightweight - Less than 2KB minified and gzipped
  • 🔗 Named Routes - Generate URLs programmatically with urlFor()
  • 🎨 Fallback Support - Handle 404s gracefully
  • 🔒 Type Safe - Full TypeScript definitions included
  • ⚙️ Framework Agnostic - Works with any framework or vanilla JS
  • 📦 Tree-Shakeable - Import only what you need
  • 🌐 Multi-Platform - Browser and Cloudflare Workers support

🚀 Quick Start

Installation

# npm
npm install @the-memoize-project/router

# yarn
yarn add @the-memoize-project/router

# bun
bun add @the-memoize-project/router

# pnpm
pnpm add @the-memoize-project/router

Basic Usage

Browser

import router from "@the-memoize-project/router/browser";

// Define routes
router("/", homePage)
  ("/about", aboutPage)
  ("/users/:id", userPage)
  .fallback(notFoundPage);

// Handle route changes
router.handle();

// Navigate programmatically
function navigate(path) {
  history.pushState({}, "", path);
  router.handle();
}

Cloudflare Workers

import router from "@the-memoize-project/router/worker";

// Define API routes with HTTP methods
router.get("/api/users/:id", getUser);
router.post("/api/users", createUser);
router.put("/api/users/:id", updateUser);
router.delete("/api/users/:id", deleteUser);

// Route handlers
async function getUser(request, env, ctx) {
  const { id } = params();
  return new Response(JSON.stringify({ id, name: "User " + id }), {
    headers: { "Content-Type": "application/json" }
  });
}

async function createUser(request, env, ctx) {
  const data = await body(request);
  return new Response(JSON.stringify({ success: true, data }), {
    status: 201,
    headers: { "Content-Type": "application/json" }
  });
}

// Export the fetch handler
export default {
  async fetch(request, env, ctx) {
    return await router.handle(request, env, ctx) ??
      new Response("Not Found", { status: 404 });
  }
};

Listening to Navigation Events (Browser)

// Listen for browser back/forward
window.addEventListener("popstate", () => {
  router.handle();
});

// Listen for link clicks
document.addEventListener("click", (e) => {
  if (e.target.matches("a[href^='/']")) {
    e.preventDefault();
    const path = e.target.getAttribute("href");
    history.pushState({}, "", path);
    router.handle();
  }
});

🌐 CDN Usage

Perfect for prototyping or learning:

import router from "https://esm.sh/@the-memoize-project/router";
import { params, urlFor } from "https://esm.sh/@the-memoize-project/router";

📦 API Reference

Browser Router API

router(path, handler)

Define a route with a path pattern and handler function.

router("/users/:id", userPage)
  ("/about", aboutPage)
  .fallback(notFoundPage);

Parameters:

  • path (string) - Route pattern (supports :param syntax)
  • handler (function) - Function to call when route matches

Returns: Router instance (chainable)


router.handle()

Match the current URL against registered routes and execute the matching handler.

// Call on page load
router.handle();

// Call after navigation
history.pushState({}, "", "/new-path");
router.handle();

router.fallback(handler)

Define a fallback handler for unmatched routes (404).

router.fallback(() => {
  console.log("404 - Page not found");
});

Parameters:

  • handler (function) - Function to call when no route matches

Worker Router API

router.get(path, handler) / router.post(path, handler) / etc.

Define routes for specific HTTP methods (GET, POST, PUT, DELETE).

router.get("/api/users/:id", getUser);
router.post("/api/users", createUser);
router.put("/api/users/:id", updateUser);
router.delete("/api/users/:id", deleteUser);

Parameters:

  • path (string) - Route pattern (supports :param syntax)
  • handler (async function) - Function to call when route matches
    • Receives (request, env, ctx) parameters
    • Must return a Response object

Returns: Router instance


router.handle(request, env, ctx)

Match the incoming request against registered routes and execute the matching handler.

export default {
  async fetch(request, env, ctx) {
    return await router.handle(request, env, ctx) ??
      new Response("Not Found", { status: 404 });
  }
};

Parameters:

  • request (Request) - Incoming HTTP request
  • env (object) - Environment bindings
  • ctx (ExecutionContext) - Execution context

Returns: Promise or null if no route matched


Helper Functions

params() (Browser & Worker)

Access route parameters from the current route.

// Browser
import { params } from "@the-memoize-project/router/browser";

// Worker
import { params } from "@the-memoize-project/router/worker";

// Route: /users/:id
// URL: /users/123
const { id } = params();
console.log(id); // "123"

Returns: Object containing route parameters


args() (Browser & Worker)

Access query string parameters from the current URL.

// Browser
import { args } from "@the-memoize-project/router/browser";

// Worker
import { args } from "@the-memoize-project/router/worker";

// URL: /search?q=router&page=2
const { q, page } = args();
console.log(q);    // "router"
console.log(page); // "2"

Returns: Object containing query parameters


body(request) (Worker only)

Parse the request body from a Cloudflare Worker request.

import { body } from "@the-memoize-project/router/worker";

async function createUser(request, env, ctx) {
  const data = await body(request);
  console.log(data); // Parsed request body
  return new Response(JSON.stringify({ success: true }));
}

Parameters:

  • request (Request) - The incoming HTTP request

Returns: Promise - Parsed request body (JSON, form data, or text)


urlFor(name, params) (Browser & Worker)

Generate a URL for a named route with parameters.

// Browser
import { urlFor } from "@the-memoize-project/router/browser";

// Worker
import { urlFor } from "@the-memoize-project/router/worker";

// Define named route
function userPage() {}
router("/users/:id", userPage);

// Generate URL
const url = urlFor("userPage", { id: 123 });
console.log(url); // "https://example.com/users/123"

Parameters:

  • name (string) - Handler function name
  • params (object) - Route parameters to interpolate

Returns: Full URL string


🎭 Showcase

Real-World Examples

Single Page Application (Browser)

import router, { params } from "@the-memoize-project/router/browser";

// Define routes
router("/", homePage)
  ("/dashboard", dashboardPage)
  ("/users/:id", userDetailPage)
  ("/users/:id/edit", userEditPage)
  .fallback(notFoundPage);

// Initialize
router.handle();

// Listen for navigation
window.addEventListener("popstate", () => router.handle());

function userDetailPage() {
  const { id } = params();
  fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(user => {
      document.body.innerHTML = `
        <h1>${user.name}</h1>
        <p>Email: ${user.email}</p>
      `;
    });
}

RESTful API with Cloudflare Workers

import router, { params, body } from "@the-memoize-project/router/worker";

// Define API routes
router.get("/api/users", listUsers);
router.get("/api/users/:id", getUser);
router.post("/api/users", createUser);
router.put("/api/users/:id", updateUser);
router.delete("/api/users/:id", deleteUser);

// Mock database (use KV, D1, or Durable Objects in production)
const users = new Map([
  [1, { id: 1, name: "Alice", email: "[email protected]" }],
  [2, { id: 2, name: "Bob", email: "[email protected]" }],
]);

async function listUsers(request, env, ctx) {
  const userList = Array.from(users.values());
  return new Response(JSON.stringify(userList), {
    headers: { "Content-Type": "application/json" }
  });
}

async function getUser(request, env, ctx) {
  const { id } = params();
  const user = users.get(Number(id));

  if (!user) {
    return new Response("User not found", { status: 404 });
  }

  return new Response(JSON.stringify(user), {
    headers: { "Content-Type": "application/json" }
  });
}

async function createUser(request, env, ctx) {
  const data = await body(request);
  const newUser = { id: users.size + 1, ...data };
  users.set(newUser.id, newUser);

  return new Response(JSON.stringify(newUser), {
    status: 201,
    headers: { "Content-Type": "application/json" }
  });
}

async function updateUser(request, env, ctx) {
  const { id } = params();
  const data = await body(request);
  const user = users.get(Number(id));

  if (!user) {
    return new Response("User not found", { status: 404 });
  }

  const updatedUser = { ...user, ...data };
  users.set(Number(id), updatedUser);

  return new Response(JSON.stringify(updatedUser), {
    headers: { "Content-Type": "application/json" }
  });
}

async function deleteUser(request, env, ctx) {
  const { id } = params();
  const deleted = users.delete(Number(id));

  if (!deleted) {
    return new Response("User not found", { status: 404 });
  }

  return new Response(null, { status: 204 });
}

export default {
  async fetch(request, env, ctx) {
    return await router.handle(request, env, ctx) ??
      new Response("Not Found", { status: 404 });
  }
};

Nested Routes with Query Parameters (Browser)

import router, { params, args } from "@the-memoize-project/router/browser";

router("/blog/:category", blogCategoryPage)
  ("/blog/:category/:post", blogPostPage);

function blogCategoryPage() {
  const { category } = params();
  const { page = 1 } = args(); // Default to page 1

  console.log(`Showing ${category} posts, page ${page}`);
}

function blogPostPage() {
  const { category, post } = params();
  console.log(`Viewing post: ${post} in ${category}`);
}

Named Routes for Link Generation (Browser)

import router, { urlFor } from "@the-memoize-project/router/browser";

function userProfile() {
  // Handler implementation
}

router("/users/:id/profile", userProfile);

// Generate links dynamically
const users = [
  { id: 1, name: "Alice" },
  { id: 2, name: "Bob" },
];

const html = users.map(user => `
  <a href="${urlFor("userProfile", { id: user.id })}">
    ${user.name}
  </a>
`).join("");

🤔 Why Memoize Router?

Comparison with Other Solutions

| Feature | Memoize Router | React Router | Vue Router | Page.js | |---------|---------------|--------------|------------|---------| | Zero Dependencies | ✅ | ❌ | ❌ | ❌ | | Framework Agnostic | ✅ | ❌ | ❌ | ✅ | | Bundle Size | ~2KB | ~45KB | ~25KB | ~5KB | | TypeScript Support | ✅ | ✅ | ✅ | ⚠️ | | Pattern Matching | ✅ | ✅ | ✅ | ✅ | | Named Routes | ✅ | ❌ | ✅ | ❌ | | No Build Required | ✅ | ❌ | ❌ | ✅ |

The Memoize Router Philosophy

Traditional Approach:

// Complex, imperative routing
const router = new Router();
router.add("/users/:id", {
  onEnter: () => {},
  onExit: () => {},
  middleware: [],
  component: UserPage,
});
router.init();

Memoize Router Approach:

// Simple, declarative routing
router("/users/:id", userPage).handle();

🗺️ Roadmap

  • [x] Core routing functionality
  • [x] Pattern matching with parameters
  • [x] Query string parsing
  • [x] Named routes
  • [x] TypeScript definitions
  • [ ] Hash-based routing
  • [ ] Route guards/middleware
  • [ ] Nested route helpers
  • [ ] Route transitions
  • [ ] Official documentation site

🛠️ Development

Prerequisites

  • Bun (recommended) or Node.js 18+

Commands

# Install dependencies
bun install

# Start development server
bun dev

# Run tests with coverage
bun run test

# Build for production
bun run build

# Lint and format
biome check .

# Auto-fix issues
biome check --write .

Project Structure

@the-memoize-project/router/
├── packages/
│   ├── browser/        # Browser-specific implementation
│   │   ├── router/     # Chainable router API
│   │   ├── args/       # Query string parser
│   │   ├── params/     # Route parameter extractor
│   │   ├── urlFor/     # Named route URL generator
│   │   ├── handle/     # Route handler executor
│   │   ├── matching/   # Route pattern matcher
│   │   ├── listeners/  # Route registry (array)
│   │   ├── fallback/   # 404 handler
│   │   ├── pushState/  # History API wrapper
│   │   └── popState/   # Back/forward navigation
│   └── worker/         # Cloudflare Workers implementation
│       ├── router/     # HTTP method router (Proxy-based)
│       ├── args/       # Query string parser
│       ├── params/     # Route parameter extractor
│       ├── urlFor/     # Named route URL generator
│       ├── handle/     # Request handler executor
│       ├── match/      # HTTP method + path matcher
│       ├── listeners/  # Route registry (by HTTP method)
│       └── body/       # Request body parser
├── dist/               # Built output (browser.js, worker.js)
├── types.d.ts          # TypeScript definitions
├── vite.config.js      # Build configuration
└── vitest.config.js    # Test configuration

🤝 Contributing

We welcome contributions! Whether you're fixing bugs, improving docs, or proposing new features, your help makes Memoize Router better.

Ways to contribute:

Please read our Contributing Guide and Code of Conduct before getting started.


📄 License

MIT © The Memoize Project Contributors

See LICENSE for details.


🙏 Acknowledgments

Memoize Router is inspired by the principles of:

  • History API - Browser-native navigation
  • URL Pattern Matching - Declarative route definition
  • Functional Programming - Composable, predictable functions
  • Minimalism - Do one thing well

📚 Resources


Built with ❤️ for The Memoize Project

⭐ Star us on GitHub · 💬 Join discussions · 🧠 Learn more about The Memoize Project