Baike.dev
All toolsOnline toolsAI codingTrendingOpen sourceNewsSubmit
Log in
< Back to tools
R

rou3

> 编程语言
Open source

Lightweight and fast rou(ter) for JavaScript

740 stars0 likes2 views
WebsiteGitHub

About

Lightweight and fast rou(ter) for JavaScript

rou3

Lightweight and fast router for JavaScript.

Usage

Install:

bash
# ✨ Auto-detect
npx nypm install rou3

Import:

ESM (Node.js, Bun, Deno)

javascript
import {
  createRouter,
  addRoute,
  findRoute,
  removeRoute,
  findAllRoutes,
  routesOverlap,
  compareRoutes,
  findOverlappingRoutes,
  routeNodeKeys,
  routeToRegExp,
  regExpToRoute,
  NullProtoObj,
} from "rou3";

CDN (Deno and Browsers)

javascript
import {
  createRouter,
  addRoute,
  findRoute,
  removeRoute,
  findAllRoutes,
  routesOverlap,
  compareRoutes,
  findOverlappingRoutes,
  routeNodeKeys,
  routeToRegExp,
  regExpToRoute,
  NullProtoObj,
} from "https://esm.sh/rou3";

Create a router instance and insert routes:

javascript
import { createRouter, addRoute } from "rou3";

const router = createRouter(/* options */);

addRoute(router, "GET", "/path", { payload: "this path" });
addRoute(router, "POST", "/path/:name", { payload: "named route" });
addRoute(router, "GET", "/path/foo/**", { payload: "wildcard route" });
addRoute(router, "GET", "/path/foo/**:name", {
  payload: "named wildcard route",
});

Match route to access matched data:

javascript
// Returns { payload: 'this path' }
findRoute(router, "GET", "/path");

// Returns { payload: 'named route', params: { name: 'fooval' } }
findRoute(router, "POST", "/path/fooval");

// Returns { payload: 'wildcard route' }
findRoute(router, "GET", "/path/foo/bar/baz");

// Returns undefined (no route matched for/)
findRoute(router, "GET", "/");

Match all routes, ordered least → most specific:

javascript
findAllRoutes(router, "GET", "/path/foo/bar/baz");
// [
//   { data: { payload: "wildcard route" } },
//   { data: { payload: "named wildcard route" }, params: { name: "bar/baz" } },
// ]

The result ordering is a documented contract — see Result ordering.

[!IMPORTANT] Paths should always begin with /.

[!IMPORTANT] Method should always be UPPERCASE.

[!TIP] If you need to register a pattern containing literal : or *, you can escape them with \\. For example, /static\\:path/\\*\\* matches only the static /static:path/** route.

Route Patterns

rou3 supports URLPattern-compatible syntax.

Pattern Example Match Params
/path/to/resource /path/to/resource {}
/users/:name /users/foo { name: "foo" }
/path/** /path/foo/bar {}
/path/**:rest /path/foo/bar { rest: "foo/bar" }
/files/*.png /files/icon.png { "0": "icon" }
/files/file-*-*.png /files/file-a-b.png { "0": "a", "1": "b" }
/users/:id(\\d+) /users/123 { id: "123" }
/files/:ext(png|jpg) /files/png { ext: "png" }
/path/(\\d+) /path/123 { "0": "123" }
/users/:id? /users or /users/123 {} or { id: "123" }
/files/:path+ /files/a/b/c { path: "a/b/c" }
/files/:path* /files or /files/a/b {} or { path: "a/b" }
/book{s}? /book or /books {}
/blog/:id(\\d+){-:title}? /blog/123 or /blog/123-my-post { id: "123" } or { id: "123", title: "my-post" }
  • Named params (:name) match a single segment.
  • Single-segment wildcards (*) capture unnamed params (0, 1, ...) and can be used as full or mid-segment tokens (for example /* or /*.png).
  • Wildcards (**) match zero or more segments. Use **:name to capture.
  • Regex constraints (:name(regex)) restrict matching. Constrained and unconstrained params can coexist on the same node (constrained checked first).
  • Unnamed groups ((regex)) capture into auto-indexed keys 0, 1, etc.
  • Modifiers: :name? (optional), :name+ (one or more), :name* (zero or more). Can combine with regex: :id(\d+)?.
  • Non-capturing groups ({...}): supported with inline (/foo{bar}) and optional (/foo{bar}?) forms.
  • Current limitation: repeating non-capturing groups ({...}+, {...}*) are supported only within a single segment (no / inside the group body).
  • Backslash escaping (\): escape special characters like :, *, (, ), {, } with a backslash (e.g., /static\:path matches literal /static:path).

Differences from URLPattern

rou3 aims for URLPattern-compatible syntax but has intentional differences due to its radix-tree design:

Feature URLPattern rou3
* (single star) Greedy catch-all (.*) across / Single-segment unnamed param ([^/]*)
** (double star) Literal ** Catch-all wildcard (zero or more segments)
(.*) in segment Greedy match across / Segment-scoped (does not cross /)
{...}+ / {...}* groups Cross-segment group repetition Only supported within a single segment (no / in group body)
Path normalization (./..) Resolves ./.. in input paths Not done by default (opt-in with { normalize: true })
Case sensitivity Can be case-insensitive Always case-sensitive
Non-/-prefixed paths Supported Paths must start with /
Unicode param names Supports Unicode identifiers Params use \w (ASCII word chars only)
Percent-encoding Normalizes %xx sequences Does not decode percent-encoded input

Path normalization

By default, findRoute and findAllRoutes do not resolve ./.. segments in input paths. If your input paths may contain relative segments, enable normalization:

javascript
findRoute(router, "GET", "/foo/bar/../baz", { normalize: true });
// Matches "/foo/baz"

findAllRoutes(router, "GET", "/foo/./bar", { normalize: true });
// Matches "/foo/bar"

The compiled router also supports this via the normalize option:

javascript
const match = compileRouter(router, { normalize: true });
match("GET", "/foo/bar/../baz"); // Matches "/foo/baz"

Result ordering

findAllRoutes returns matches ordered least → most specific: the broadest scopes first, the most specific match last. The compiled matchAll (see Compiler) returns the exact same results in the exact same order. This ordering is a contract, not incidental behavior — merge/fold-style consumers (e.g. route rules resolved by merging all matched layers, taking the last as the most specific) can rely on it, and it is pinned by tests. Any intentional change to it would be a breaking change.

javascript
const router = createRouter();
addRoute(router, "GET", "/**", { name: "catch-all" });
addRoute(router, "GET", "/api/**", { name: "api" });
addRoute(router, "GET", "/api/:v/users/:id", { name: "user" });

findAllRoutes(router, "GET", "/api/v1/users/42").map((m) => m.data.name);
// ["catch-all", "api", "user"]

Precisely:

  • Across the tree: at each level, wildcard (**) matches are emitted first, then single-segment params (*, :name), then static segments — so wilder/shallower routes come before more-static/deeper ones.

  • Same-node siblings (multiple routes ending on the same dynamic node, e.g. /foo/* and /foo/:id(\d+)): ordered by ascending specificity — optional/unconstrained entries before required/regex-constrained ones — with insertion order preserved on ties.

  • Subsumption consistency (patterns without optional syntax): when the registered patterns use no optional syntax and are strictly ordered by containment (each a "superset" of the next per compareRoutes), the result order agrees with the subsumption order (broader first).

  • Carve-out — optional syntax: a pattern containing :name?, :name* or {...}? registers several entries (one per expansion), and results are ordered by the specificity of the entry that matched, not by the breadth of the whole pattern. A pattern that is a "superset" of another can therefore come last:

    javascript
    const router = createRouter();
    addRoute(router, "GET", "/admin", { name: "admin" });
    addRoute(router, "GET", "/admin/:page?", { name: "admin-page" }); // superset of "/admin"
    
    findAllRoutes(router, "GET", "/admin").map((m) => m.data.name);
    // ["admin", "admin-page"] — the broader pattern is last

    If you need a true pattern-level containment order and your patterns may use optional syntax, re-sort the (small) result array yourself with compareRoutes.

  • Registration order never affects the result order, except as the tiebreaker between equally specific same-node entries — which, per the carve-out above, includes expansions of optional-syntax patterns (registering /admin/:page? before /admin swaps the two results in the example above).

findOverlappingRoutes follows the same least → most specific order.

Pattern overlap

findRoute/findAllRoutes match a concrete path against registered patterns. Sometimes you instead need to reason about patterns against patterns — e.g. to resolve an "effective" merged config over a whole scope, you need to know when two patterns can match a common concrete path.

Three utilities cover this:

…
  • routesOverlap(patternA, patternB) — returns true if the two patterns' match-sets intersect (there exists a concrete path matched by both). This is overlap, not subset containment.

  • compareRoutes(patternA, patternB) — classifies the relation between the two match-sets. Verdicts follow the ES2025 Set-method vocabulary (isSupersetOf/isSubsetOf/isDisjointFrom) and are directional — read them as "patternA is a … of patternB":

    • "disjoint" — provably no common path.
    • "equal" — provably the same paths. Param names are ignored: /a/:x equals /a/:y, /u/:id(\d+) equals /u/:x(\d+), and /a/:x? equals /a/*.
    • "superset" — patternA provably matches every path patternB matches (strict unless equality is undecidable).
    • "subset" — the mirror image (patternA ⊆ patternB).
    • "partial" — no containment proven; the sets may intersect.

    Useful for ordering patterns by specificity and detecting ambiguous pairs where "most specific match" is undefined. Every verdict's containment claims are proofs, and undecidable cases degrade to a weaker verdict, never a wrong claim: containment between two different regex constraints falls back to `

Issues· 0 open

View all issuesOpen on GitHub

No open issues yet, or sync has not completed.

> Tags

TypeScript

No comments yet. Be the first to share.

> Details

PublishedAug 1, 2026
UpdatedSep 17, 2026
Category编程语言
PricingOpen source

> Related tools

T
TypeScript
JavaScript 的超集,为前端与全栈提供静态类型
P
Python
通用编程语言,广泛用于 Web、数据与 AI
G
Go
Google 推出的简洁高效系统语言