CLI tool
h3js/rou3 avatar
h3js/rou3

rou3 keeps HTTP routing in a tree instead of a list of regular expressions

🌳 Lightweight and fast rou(ter) for JavaScript

758 stars43 forksTypeScriptMIT

At a glance

What is it?
rou3 is a zero-dependency, tree-based router for JavaScript that speaks URLPattern-like syntax, compiles route tables into single functions, and ships utilities for comparing and converting routes. It fits projects that want match speed without a framework, but its wildcard and naming rules have edges you must read before adopting.
Who is it for?
rou3 suits engineers building a small HTTP layer, a reverse proxy, or an edge handler where pattern matching sits on the hot path and a full framework is unwanted, and it is a reasonable benchmark target for anyone comparing tree routers against flat regular expression tables. It is a poor fit for a team that needs a large plugin ecosystem, a middleware stack, or CommonJS compatibility without a wrapper.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 20 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A router built as a radix tree, not a scan over patterns

rou3 stores routes in a tree where each node corresponds to one segment of a path. Matching walks that tree segment by segment instead of testing every registered pattern against the incoming path. The README describes the design as tree-based lookups with one node per path segment, plus a fast path for static routes, so a request for a fixed path can resolve without touching the pattern machinery at all. The manifest lists "radix-tree" among the package keywords, which matches that description.

The public surface is small and functional. A router is a plain object created by createRouter, and routes are added with addRoute, which takes the router, an HTTP method, a pattern string, and any data you want back on a match. Handlers, config objects, or ids all work as route data because the router stores the value and returns it unchanged. Lookup happens through findRoute for a single best match and findAllRoutes when you want every candidate ordered from least to most specific. removeRoute deletes an entry using the same pattern string that registered it, so the pattern is effectively the route identifier.

That gives you a routing layer without a server, without middleware, and without a request or response abstraction. rou3 matches strings, which is why the same tree can serve HTTP paths, CDN routes, file maps, or any other string namespace you need to resolve.

Static segments first, then the one branch that matches

The tree resolves a path in stages. A path like /docs/guide/intro is split on the forward slash into three segments, and each segment is looked up as a child of the current node. When a segment contains no dynamic part, it is a static child and the lookup is a plain map access, which is the fast path the README mentions. When a segment contains a parameter, a regex constraint, or a wildcard, the router has to consider more than one child, and the order of those checks determines which match wins.

The consequence for you is that pattern specificity is decided by the shape of the tree rather than by an explicit priority field. The quick start shows two routes competing for the same path: a catch-all and a more precise parameterised route. findAllRoutes returns them least specific first, so the wildcard entry comes back before the parameterised one. That ordering is what lets a caller pick the most specific route itself, rather than having the router make an irreversible choice.

Static segments are also the cheapest part of the whole design. If a large fraction of your routes are literal paths, the tree spends most of its time on map lookups, and the pattern compiler has little work to do for those branches.

URLPattern-like syntax, and the segment traps inside it

The pattern language follows the URLPattern API rather than Express-style string matching. Parameters are written as :name, may carry a regular expression constraint as :name(regex), and may be repeated across segments with the plus and star modifiers. An unnamed group written as (regex) captures into a numbered key such as "0". Unnamed captures inside a segment are split the same way URLPattern splits them, so a pattern for a file name and extension yields two separate params.

Two rules catch newcomers out. First, a question mark inside a segment makes only the parameter optional, not the segment, so a pattern that makes just the parameter optional will still not match the bare parent path; the segment itself becomes optional only when it is wrapped in a group. Second, the star and plus modifiers cannot consume empty segments, so a multi-segment parameter will not match a path containing a doubled slash. The README points at a catch-all form as the way to accept empty segments.

There is a third rule about names. Parameter names must be ASCII identifiers, and a hyphen ends a name, so a pattern that reads :user-id is parsed as the parameter user followed by the literal text -id. The README links to a separate reference page for the naming rules, which tells you the hyphen behaviour is deliberate and specified rather than accidental. If you plan to use hyphens in parameter names, read that page before writing patterns.

Installing rou3 and registering the first three routes

The install step in the README is a single auto-detecting command, which works with any of the package managers the tool recognises:

bash
npx nypm install rou3

The package is ESM only. The README states that it runs on Node.js 20.19 and newer, on Bun, on Deno, and in modern browsers, and the manifest sets the engines field to a Node.js version of 20.19.0 or above. If your project still consumes CommonJS, you will need dynamic import or a bundler that handles ESM, and that is a constraint worth settling before anything else.

Once installed, you create a router and register routes. This is the quick start block from the README, with the import line and three registrations:

js
import { createRouter, addRoute, findRoute, findAllRoutes, removeRoute } from "rou3";
const router = createRouter();
addRoute(router, "GET", "/about", { page: "about" });
addRoute(router, "GET", "/users/:id", { page: "user" });
addRoute(router, "GET", "/docs/**", { page: "docs" });

Note that the method is part of the route key, so the same path can exist under different methods without collision. The router is mutable, and removeRoute takes the router, the method, and the pattern that was used to add the route, which means your route table can be built at runtime from a database or a configuration file.

Reading the match result, and catching a missing path

findRoute returns a small object or undefined, and the shape tells you where every captured value came from. For a named parameter the value lands under its name, and for a wildcard the value lands under a numbered key:

js
findRoute(router, "GET", "/users/42");
// { data: { page: "user" }, params: { id: "42" } }
findRoute(router, "GET", "/missing");
// undefined

A path that matches nothing produces undefined rather than an empty result object, so a missing route is easy to detect. A parameter that did not match contributes no key at all, so the params object can be sparse, which matters if you read values straight out of it without a default.

When several patterns match the same path, findAllRoutes is the way to inspect the field. The README registers an extra route and then lists both matches:

js
findAllRoutes(router, "GET", "/docs/guide/intro");
// [
//   { data: { page: "docs" }, params: { "0": "guide/intro", _: "guide/intro" } },
//   { data: { page: "intro" }, params: { section: "guide" } },
// ]

That output also shows the deprecated catch-all key. A double-star pattern populates a numbered entry and, for backwards compatibility, an entry under the legacy `_` key that the README marks as deprecated. Newer patterns can name the catch-all explicitly, and the migration guide referenced in the README covers upgrades from version 0.9 and older.

The compiler turns a route table into one function

rou3 can compile a router into a single optimised function, either at runtime or at build time. The README lists this as one of the headline features, and the manifest exposes it as a separate entry point, with the root export pointing at the compiled index bundle and a companion entry for the compiler. That split matters for bundle size, since a build step can precompute the matcher and drop the tree from the shipped code.

The utilities in the same package target the awkward part of routing work. The README describes helpers that compare patterns, find overlapping routes, and convert routes to and from RegExp. Overlap detection is the one worth calling out, because two patterns that both match the same request are a silent source of wrong results in most routers, and a function that lists them lets you fail a test instead of shipping a bug.

The RegExp conversion is what makes rou3 comparable to a table-of-patterns approach. You can export a route as a regular expression, feed it to an existing matcher, or accept regular expressions written by someone else and inspect how they interact with your own patterns. The pattern comparison helpers exist for the same reason: to check a new pattern against what is already registered before it goes live.

What the README does not publish is a benchmark table. The manifest defines bench scripts for Node, Bun and Deno, and the dev dependencies include an older published version of the package pinned under the name rou3-latest, which suggests the benchmarks compare the current code against the previous release. No numbers appear in the documentation, so any performance claim you make should come from running those scripts yourself rather than from the README.

Where rou3 is the wrong shape for the job

rou3 resolves strings and nothing else. There is no request object, no response helper, no middleware chain, no body parsing and no error handling convention in the documented API. If your project needs a framework that owns the request lifecycle, a router alone will not carry it, and adding one means bolting a second layer on top of the first.

The pattern language also has documented edges that will bite during a migration. The hyphen rule means some parameter names that look natural to you will not parse the way you expect. Optional segments need explicit grouping. The star and plus modifiers refuse empty segments. And the legacy catch-all key, written as `_`, still appears in output while being marked deprecated, so any code that reads it today is work you will have to revisit.

Two further limits deserve a decision rather than an assumption. The package is ESM only, which closes it off from CommonJS toolchains that have not moved yet. And the manifest lists the version as 1.0.0 while the recent release list for the project shows tags such as v0.10.0, v0.11.0 and v0.12.0, so do not assume which numbering a given published artifact carries; check the version in the package you actually install. A third limit is the licence, not the tool: rou3 ships under the MIT licence, whose main obligation is keeping the copyright and licence notice in copies of the code, though licence compatibility with the rest of your stack is worth confirming on your own terms.

Editorial conclusion

rou3 suits engineers building a small HTTP layer, a reverse proxy, or an edge handler where pattern matching sits on the hot path and a full framework is unwanted, and it is a reasonable benchmark target for anyone comparing tree routers against flat regular expression tables. It is a poor fit for a team that needs a large plugin ecosystem, a middleware stack, or CommonJS compatibility without a wrapper. Before adopting, check three things in your own routes: whether your patterns rely on hyphenated parameter names, whether you depend on the deprecated legacy catch-all key, and whether the ESM-only package layout matches your build. Then read the migration notes for anyone arriving from version 0.9 or older, because the syntax changed.

Frequently asked questions

Is rou3 still maintained, and what is the most recent release?

The repository is not archived and the last push was on 2026-10-03. Recent releases include v0.10.0 on 2026-09-28, v0.11.0 on 2026-09-29 and v0.12.0 on 2026-10-02, so the change rate has been steady over the last month.

How do I install rou3 and does it work with CommonJS?

The README gives a single install command, npx nypm install rou3, and states that rou3 is ESM only. It runs on Node.js 20.19 and newer, Bun, Deno and modern browsers, and the manifest requires a Node.js engine version of 20.19.0 or above.

How do I match a path with several segments, such as a wildcard route?

Use the URLPattern-like forms documented in the README: a double star pattern captures the remaining segments under a numbered key plus the deprecated legacy key, while a named catch-all captures the same value under a name you choose. The plus and star modifiers cannot consume empty segments.

Can rou3 detect conflicting routes before they cause wrong responses?

Yes. The README lists utilities to compare patterns, find overlapping routes, and convert routes to and from RegExp, so you can check a candidate pattern against the routes already registered.

Official sources

  1. h3js/rou3 on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/h3js-rou3.svg)](https://hysenlabs.com/projects/h3js-rou3)