Skip to content

Defining Routes

Routes are defined using the createRouter function, which provides a chainable builder API for registering URL patterns, components, middleware, prefetch handlers, and more.

The createRouter function takes a callback that receives a route factory. Call it with a URL pattern and chain methods to configure the route:

import { createRouter } from "@studiolambda/router/react";
const router = createRouter(function (route) {
route("/").render(Home);
route("/about").render(About);
route("/contact").render(Contact);
});

Each route(pattern) call returns a builder. The .render(Component) method finalizes the route, registering it in the underlying trie-based matcher with the given component.

λ Router supports three types of URL segments:

Exact literal matches. These have the highest priority when multiple patterns could match.

route("/users/settings").render(UserSettings);
route("/blog/archive").render(BlogArchive);

Capture a single URL segment into a named parameter using the :name syntax. Access the captured values with the useParams hook.

route("/user/:id").render(UserProfile);
route("/post/:slug").render(BlogPost);
route("/org/:orgId/team/:teamId").render(Team);

In the component:

import { useParams } from "@studiolambda/router/react";
function UserProfile() {
const { id } = useParams();
return <h1>User {id}</h1>;
}

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

route("/files/*path").render(FileViewer);
route("/docs/*").render(DocsPage);

A bare * without a name captures into params["*"].

function FileViewer() {
const { path } = useParams();
// URL: /files/images/photo.jpg
// path = "images/photo.jpg"
return <div>Viewing: {path}</div>;
}

When multiple patterns could match a URL, the trie uses this priority order at each segment level:

  1. Static — exact literal match (highest priority)
  2. Dynamic:param captures
  3. Wildcard*param catch-all (lowest priority)

This means /users/settings always takes precedence over /users/:id for the URL /users/settings.

Groups let you share configuration (path prefixes, middleware, prefetch handlers) across multiple routes without repeating yourself. Call .group() on a builder to get a new route factory scoped to that configuration:

const router = createRouter(function (route) {
route("/").render(Home);
// All routes under /dashboard share the prefix and middleware.
const dashboard = route("/dashboard").middleware([AuthGuard]).group();
dashboard("/").render(DashboardHome);
dashboard("/analytics").render(Analytics);
dashboard("/settings").render(Settings);
// Nested groups inherit parent configuration.
const admin = dashboard("/admin").middleware([AdminOnly]).group();
admin("/users").render(AdminUsers);
admin("/logs").render(AdminLogs);
});

In this example:

  • dashboard("/analytics") registers the pattern /dashboard/analytics with AuthGuard middleware.
  • admin("/users") registers /dashboard/admin/users with both AuthGuard and AdminOnly middleware (parent middleware wraps outermost).

You can call route() without a path to create a group that only shares middleware or prefetch configuration without contributing a path segment:

const router = createRouter(function (route) {
// These routes all have AuthGuard but no shared path prefix.
const authed = route().middleware([AuthGuard]).group();
authed("/profile").render(Profile);
authed("/settings").render(Settings);
authed("/billing").render(Billing);
});

Use .redirect(target) to register a route that redirects to another URL during the precommit phase (before the URL bar updates):

const router = createRouter(function (route) {
route("/old-page").redirect("/new-page");
});

Redirects happen at the Navigation API level — the browser never commits the original URL to the address bar. The redirect target is an absolute path.

The .redirect() method also accepts a callback function that receives the PrefetchContext and returns the target path. This is useful when the redirect target depends on the matched route parameters or the destination URL:

const router = createRouter(function (route) {
// Carry the :id param to the new URL.
route("/old-user/:id").redirect(function ({ params }) {
return `/user/${params.id}`;
});
// Preserve search parameters across a redirect.
route("/old-search").redirect(function ({ url }) {
return `/search${url.search}`;
});
});

The callback receives the same PrefetchContext object that prefetch handlers receive (params, url, and controller). The returned string is the absolute path to redirect to — it is not prefixed by parent groups.

All builder methods (except .render(), .redirect(), and .group()) return the builder for chaining:

route("/dashboard")
.middleware([AuthGuard])
.prefetch(loadDashboardData)
.scroll("after-transition")
.focusReset("after-transition")
.render(Dashboard);
MethodDescription
.middleware(list)Appends middleware components. Inherited group middleware wraps outermost.
.prefetch(fn)Adds a prefetch function that runs before URL commits. Parent prefetches run first.
.scroll(behavior)Sets scroll restoration: "after-transition" (default) or "manual".
.focusReset(behavior)Sets focus reset: "after-transition" (default) or "manual".
.formHandler(fn)Sets a handler for POST form submissions matching this route.
.render(Component)Registers the route with the given component (terminal).
.redirect(target)Registers a redirect to the target path or callback (terminal).
.group()Returns a scoped route factory inheriting this builder’s config (terminal).

When using groups, configuration is inherited with the following rules:

  • Path prefixes are concatenated: route("/a").group() then child("/b")/a/b.
  • Middleware is prepended: parent middleware wraps outermost, then child middleware wraps inner.
  • Prefetch handlers chain sequentially: parent prefetch runs first, then child prefetch.
  • Scroll, focus reset, and form handler are set per-route and not inherited.