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.
Creating a Matcher
Section titled “Creating a Matcher”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"}Registering Routes
Section titled “Registering Routes”Use matcher.register(pattern, handler) to add routes. Patterns are /-separated strings that support three segment types.
Static Segments
Section titled “Static Segments”Exact literal matches. These have the highest priority.
matcher.register("/users/settings", handleSettings);matcher.register("/blog/archive", handleArchive);Dynamic Segments
Section titled “Dynamic Segments”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);Wildcard Segments
Section titled “Wildcard Segments”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);Matching URLs
Section titled “Matching URLs”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.
Dynamic Parameters
Section titled “Dynamic Parameters”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" }Matching Priority
Section titled “Matching Priority”When multiple patterns could match a URL, the trie evaluates candidates at each segment level in this order:
- Static — exact literal match (highest priority)
- Dynamic —
:paramsegment - Wildcard —
*paramcatch-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 segmentsmatcher.match("/files/a/b/c")?.handler; // handleFiles (wildcard fallback)matcher.match("/files/a")?.handler; // handleFile (dynamic wins for single segment)Catch-All Routes
Section titled “Catch-All Routes”Register a root wildcard to catch all unmatched paths:
matcher.register("/*", handleNotFound);
matcher.match("/anything/at/all")?.params;// { "*": "anything/at/all" }API Reference
Section titled “API Reference”createMatcher<T>(options?)
Section titled “createMatcher<T>(options?)”Creates a new matcher instance.
| Parameter | Type | Description |
|---|---|---|
options.root | Node<T> | Optional pre-built trie root node. Useful for sharing route trees. |
Returns a Matcher<T> with register and match methods.
Matcher<T>
Section titled “Matcher<T>”| Method | Signature | Description |
|---|---|---|
register | (pattern: string, handler: T) => void | Registers a route pattern with a handler. |
match | (path: string) => Resolved<T> | null | Matches a URL path against registered routes. Returns null if no match. |
Resolved<T>
Section titled “Resolved<T>”The result of a successful match.
| Property | Type | Description |
|---|---|---|
handler | T | The handler registered for the matched pattern. |
params | Record<string, string> | Extracted dynamic and wildcard parameters. |
Node<T>
Section titled “Node<T>”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.
| Property | Type | Description |
|---|---|---|
children | Map<string, Node<T>> | Static child segments keyed by literal string. |
handler | T | undefined | The handler at this node, or undefined for intermediate nodes. |
child | Node<T> | undefined | The dynamic (:param) child node. |
name | string | undefined | The parameter name for dynamic nodes (without the : prefix). |
wildcard | Node<T> | undefined | The wildcard (*param) child node. |
Example: Simple Server Router
Section titled “Example: Simple Server Router”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);Relationship to the React Integration
Section titled “Relationship to the React Integration”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} />;}