Skip to content

Pattern Matching

The core of λ Router is a trie-based URL pattern matcher exported from the root package entry point. It has zero dependencies, no framework coupling, and works in any JavaScript runtime — browsers, Node.js, Deno, Bun, Cloudflare Workers, or anywhere else.

import { createMatcher } from "@studiolambda/router";

The React integration (@studiolambda/router/react) is built on top of this matcher. If you only need URL pattern matching without React, you can use createMatcher directly.

Call createMatcher<T>() with the type of handler you want to associate with each route. The handler type is completely up to you — it could be a function, a string, a component, or any value:

import { createMatcher } from "@studiolambda/router";
const matcher = createMatcher<() => string>();
matcher.register("/", function () {
return "home";
});
matcher.register("/about", function () {
return "about";
});
const match = matcher.match("/about");
if (match) {
const result = match.handler(); // "about"
}

Use matcher.register(pattern, handler) to add routes. Patterns are /-separated strings that support three segment types.

Exact literal matches. These have the highest priority.

matcher.register("/users/settings", handleSettings);
matcher.register("/blog/archive", handleArchive);

Capture a single URL segment into a named parameter using the :name syntax:

matcher.register("/user/:id", handleUser);
matcher.register("/org/:orgId/team/:teamId", handleTeam);

Capture all remaining segments using the *name syntax. Wildcards must be the last segment in a pattern. The captured value is the remaining path joined by /:

matcher.register("/files/*path", handleFile);

A bare * without a name captures into a parameter named "*":

matcher.register("/catch/*", handleCatchAll);

Call matcher.match(path) with a URL path. It returns a Resolved<T> object on match, or null if no route matches:

const match = matcher.match("/user/42");
if (match) {
match.handler; // the handler registered for /user/:id
match.params; // { id: "42" }
}

Trailing slashes are ignored — /foo/bar and /foo/bar/ match the same route.

Parameters are extracted from :param and *param segments and returned in the params record:

matcher.register("/user/:id", "user-handler");
matcher.register("/user/:id/files/*path", "files-handler");
matcher.match("/user/42")?.params;
// { id: "42" }
matcher.match("/user/42/files/docs/readme.md")?.params;
// { id: "42", path: "docs/readme.md" }

When multiple patterns could match a URL, the trie evaluates candidates at each segment level in this order:

  1. Static — exact literal match (highest priority)
  2. Dynamic:param segment
  3. Wildcard*param catch-all (lowest priority)
matcher.register("/files/*path", handleFiles);
matcher.register("/files/:id", handleFile);
matcher.register("/files/exact", handleExact);
matcher.match("/files/exact")?.handler; // handleExact (static wins)
matcher.match("/files/something")?.handler; // handleFile (dynamic wins over wildcard)
matcher.match("/files/a/b/c")?.handler; // handleFiles (wildcard captures the rest)

If a dynamic segment matches a single segment but there is no deeper route registered, the matcher falls back to the wildcard:

matcher.register("/files/:id", handleFile);
matcher.register("/files/*path", handleFiles);
// :id matches "a" but there's no route at /files/:id with deeper segments
matcher.match("/files/a/b/c")?.handler; // handleFiles (wildcard fallback)
matcher.match("/files/a")?.handler; // handleFile (dynamic wins for single segment)

Register a root wildcard to catch all unmatched paths:

matcher.register("/*", handleNotFound);
matcher.match("/anything/at/all")?.params;
// { "*": "anything/at/all" }

Creates a new matcher instance.

ParameterTypeDescription
options.rootNode<T>Optional pre-built trie root node. Useful for sharing route trees.

Returns a Matcher<T> with register and match methods.

MethodSignatureDescription
register(pattern: string, handler: T) => voidRegisters a route pattern with a handler.
match(path: string) => Resolved<T> | nullMatches a URL path against registered routes. Returns null if no match.

The result of a successful match.

PropertyTypeDescription
handlerTThe handler registered for the matched pattern.
paramsRecord<string, string>Extracted dynamic and wildcard parameters.

A trie node representing a single URL segment. You generally don’t create nodes directly, but the type is exported for advanced use cases like serializing or sharing pre-built route trees.

PropertyTypeDescription
childrenMap<string, Node<T>>Static child segments keyed by literal string.
handlerT | undefinedThe handler at this node, or undefined for intermediate nodes.
childNode<T> | undefinedThe dynamic (:param) child node.
namestring | undefinedThe parameter name for dynamic nodes (without the : prefix).
wildcardNode<T> | undefinedThe wildcard (*param) child node.

The matcher is useful outside of React. Here is a minimal HTTP server router using Node’s built-in http module:

import { createServer } from "node:http";
import { createMatcher } from "@studiolambda/router";
type Handler = (
req: import("node:http").IncomingMessage,
res: import("node:http").ServerResponse,
) => void;
const matcher = createMatcher<Handler>();
matcher.register("/", function (req, res) {
res.end("Home");
});
matcher.register("/user/:id", function (req, res) {
res.end("User page");
});
createServer(function (req, res) {
const match = matcher.match(new URL(req.url!, `http://${req.headers.host}`).pathname);
if (match) {
match.handler(req, res);
} else {
res.statusCode = 404;
res.end("Not Found");
}
}).listen(3000);

The React layer (@studiolambda/router/react) uses createMatcher<Handler> internally. The createRouter builder API is a convenience that constructs a matcher and registers routes with React-specific handler objects (component, middleware, prefetch, etc.). The <Router> component accepts any Matcher<Handler> via its matcher prop.

If you need custom route registration logic that the builder API doesn’t support, you can call createMatcher<Handler>() directly and pass it to <Router>:

import { createMatcher } from "@studiolambda/router";
import { Router, type Handler } from "@studiolambda/router/react";
const matcher = createMatcher<Handler>();
matcher.register("/", { component: Home });
matcher.register("/about", {
component: About,
prefetch: async function ({ params, url }) {
// custom prefetch logic
},
});
function App() {
return <Router matcher={matcher} />;
}