Open-source project
ibraheemdev/matchit avatar
ibraheemdev/matchit

matchit stores routes in a radix trie so lookups stay cheap

A high performance, zero-copy URL router.

539 stars56 forksRustMIT

At a glance

What is it?
matchit is a MIT licensed Rust URL router that keeps routes in a radix trie and hands back parameter slices without allocating, with named parameters, prefixed and suffixed parameters, catch-alls, and an explicit set of conflict rules that reject ambiguous routes. Version 0.9.2 is the current release.
Who is it for?
Adopt matchit if you are writing a Rust HTTP service and want a router with no allocation on the matching path and a documented conflict model, since the rules for what may overlap are written down and enforced at insert time. Skip it if your routing needs are a handful of fixed paths, since the trie buys you nothing there, and note that catch-all parameters always conflict with suffixed ones.
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?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Rust, 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

What the crate gives you in one insert and one lookup

The README opens with a complete program, and it is worth reading closely because it shows the shape of the whole API. You create a Router, insert paths with values of your choosing, and look up with at. A match returns both the value and the parameters.

rust
use matchit::Router;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut router = Router::new();
    router.insert("/home", "Welcome!")?;
    router.insert("/users/{id}", "A User")?;

    let matched = router.at("/users/978")?;
    assert_eq!(matched.params.get("id"), Some("978"));
    assert_eq!(*matched.value, "A User");

    Ok(())
}

The insert takes a path and a value, and it returns a Result, which matters because a router can reject a route. The lookup takes a path and also returns a Result, so a miss is an error rather than an Option. The params object is what makes this worth the trouble: get returns a borrowed slice of the original path rather than a freshly allocated String, which is where the zero-copy claim comes from. The value is generic, so the router does not care whether you are storing a static string, a handler identifier or a struct.

The four kinds of parameter, and their exact matching rules

The router supports dynamic route segments, and there are four distinct forms. A named parameter such as /{id} matches anything until the next static segment or the end of the path, so /users/{id} matches /users/1 and /users/23 but not /users, which is an error. A parameter with a prefix or a suffix, such as /images/img-{id}.png, matches only within that shape, so /images/img-1.png matches and /images/img-1.jpg does not. The README states the constraint plainly: there may only be a single named parameter per route segment. A catch-all parameter starts with a star and matches anything until the end of the path, so /{*rest} captures static/bar.css as well as foo.html, and it must always be at the end of the route. It also refuses the root path, since / would lead to an empty parameter value. And the literal braces can be escaped by doubling them, so {{ and }} produce a literal brace in a static route, which is what lets /{{hello}} and /{hello} coexist without ambiguity.

The conflict rules, and why ambiguity is an error not a warning

The formal description of what may overlap is the part of the documentation worth studying. A route is a list of segments separated by a slash with an optional leading and trailing slash. Given a set of routes, their overlapping segments may include any number of static segments, then one of: any number of route parameters with a suffix, prioritising the longest suffix, or any number of route parameters with a prefix, prioritising the longest prefix, or a single route parameter with both a prefix and a suffix, then one of: a single standalone parameter, or a single standalone catch-all parameter, which applies only to the final segment. Any other combination of route segments is considered ambiguous, and attempting to insert such a route results in an error. When static and dynamic segments overlap, static segments win, which the README demonstrates with a root route and /about at priority 1 against a catch-all at priority 2.

The catch-all exception that the README flags as provisional

There is one documented exception to the conflict rules, and the README is candid about it being an implementation detail rather than a design choice. Catch-all parameters are always considered to conflict with suffixed route parameters, so /{*rest} and /{x}suffix are treated as overlapping even though the formal rules above would allow them. The README says this is due to an implementation detail of the routing tree that may be relaxed in the future. That is worth knowing before you design a route table around it, because a rule that may be relaxed later is a rule you should not depend on. It also means the insertion error you get for that particular pair is a false conflict by the letter of the specification, and if you hit it the workaround is to restructure the routes rather than to expect the crate to accept them.

How the radix trie reduces the search

The explanation section gives the reason the crate is fast, and it starts from an observation about URL structure. The router takes advantage of the fact that URL routes generally follow a hierarchical structure, and routes are stored in a radix trie that makes heavy use of common prefixes. The README shows the resulting tree with a priority number, a path column and a value column, where shared prefixes collapse into single nodes and the numbers indicate the order in which branches are tried. This reduces the route search to a small number of branches. There is a second refinement on top of the trie: child nodes at the same level are prioritised by the number of children with registered values, which increases the chance of choosing the correct branch on the first try. That is a heuristic, and heuristics in routers tend to be where the surprises live, so it is worth knowing it is there.

Benchmarks against seven other routers

The README includes benchmark output for a match against 130 registered routes, with the benchmark code linked in the benches directory of the repository. The numbers given are 2.4451 to 2.4462 microseconds for matchit, 4.2618 to 4.2646 for gonzales, 4.8666 to 4.8728 for path-tree, 4.9440 to 4.9668 for wayfind, 49.203 to 49.226 for route-recognizer, 70.598 to 70.670 for routefinder, 453.91 to 454.11 for actix and 421.76 to 421.89 for regex. The comparison set is worth noting for what it implies. The regex and actix figures are the price of a regex-based or framework-bound router, and route-recognizer and routefinder sit in between, which suggests matchit is competitive with the other tree-based approaches rather than uniquely fast. Treat these as the crate's own numbers on one machine with one route set, not as a general ranking.

Where matchit is the wrong choice

Three limits. The first is that matchit is a router and nothing else. The Cargo.toml has no runtime dependencies at all, and the dev-dependencies are the benchmark and example stack, criterion, actix-router, regex, route-recognizer, gonzales, path-tree, routefinder and wayfind, plus tower, tokio, http-body-util, hyper and hyper-util for the examples. The single example file in the repository, examples/hyper.rs, is a hyper server. So matchit gives you path matching and parameter extraction and leaves the entire HTTP layer to you, which is the right split if you want control and the wrong choice if you wanted a web framework. The second is the conflict model. Because ambiguous combinations are rejected at insert time, a route table with many overlapping dynamic segments has to be designed rather than accumulated, and the catch-all exception means one specific pattern is off limits. The third is version churn in a small crate. It is at 0.9.2 with a rust-version of 1.66 and edition 2021, the last push was on 2026-04-08, and the licence is MIT AND BSD-3-Clause with a separate LICENSE.httprouter file, since a lot of the code was inspired by Julien Schmidt's httprouter. A 0.x version means the API can still move, which is normal for a crate this young and worth noting if you are building a service on it.

Editorial conclusion

Adopt matchit if you are writing a Rust HTTP service and want a router with no allocation on the matching path and a documented conflict model, since the rules for what may overlap are written down and enforced at insert time. Skip it if your routing needs are a handful of fixed paths, since the trie buys you nothing there, and note that catch-all parameters always conflict with suffixed ones. Before you commit to a route set, insert it and let the router reject any ambiguous combination rather than discovering the problem at request time.

Frequently asked questions

What is matchit?

It is a high performance, zero-copy URL router written in Rust, published on crates.io as matchit with documentation on docs.rs. Routes are stored in a radix trie that makes heavy use of common prefixes, and matched parameters are returned as borrowed slices of the request path.

What kinds of route parameters does matchit support?

Named parameters like /{id} that match until the next static segment, parameters with a prefix or suffix such as /images/img-{id}.png, catch-all parameters starting with * that must be at the end of the route, and literal braces escaped by doubling them. There may only be a single named parameter per route segment.

What happens when I insert two conflicting routes in matchit?

Static and dynamic segments may overlap, with static segments taking priority, but any other ambiguous combination is refused and the insert returns an error. The one documented exception is that catch-all parameters always conflict with suffixed route parameters, which the README attributes to an implementation detail of the routing tree.

How does matchit store routes?

In a radix trie, which exploits the fact that URL routes generally follow a hierarchical structure. This reduces the route search to a small number of branches, and child nodes on the same level are prioritised by the number of children with registered values.

Official sources

  1. ibraheemdev/matchit on GitHub
  2. License: MIT
  3. Project website
  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/ibraheemdev-matchit.svg)](https://hysenlabs.com/projects/ibraheemdev-matchit)

Was this useful?