summaryrefslogtreecommitdiff
path: root/README.md
diff options
context:
space:
mode:
authorAsko Nõmm <asko@nmm.ee>2026-02-17 20:29:53 +0200
committerAsko Nõmm <asko@nmm.ee>2026-02-17 20:29:53 +0200
commitd3058cd7e742771d97ec81c9e4ae1e96f954d4a4 (patch)
treef13ed9f447ee3b3bb62074d5e8d95bf9bf56ebfa /README.md
parent1e2a95e4dab2c7b82c168a6a0fdce7d7485b4a8c (diff)
2.0: Improve performance, usability.
This is most likely a breaking change. Though from the API nothing changes, behaviour does. It will no longer match routes based on the first match, but rather the best match (the most specific route wins).
Diffstat (limited to 'README.md')
-rw-r--r--README.md98
1 files changed, 62 insertions, 36 deletions
diff --git a/README.md b/README.md
index 5bbba5f..4cb6802 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,13 @@ A tiny, zero dependency, system-agnostic router for Clojure, ClojureScript, Baba
## Installation
-[![Clojars Project](https://img.shields.io/clojars/v/org.clojars.askonomm/ruuter.svg)](https://clojars.org/org.clojars.askonomm/ruuter)
+Add Ruuter as a git dependency in your `deps.edn`:
+
+```clojure
+{:deps {askonomm/ruuter {:git/url "https://git.nmm.ee/asko/ruuter.git"
+ :git/tag "v2.0.0"
+ :git/sha "<sha>"}}}
+```
## Usage
@@ -28,7 +34,7 @@ Require the namespace `ruuter.core` and then pass your routes to the `route` fun
; :body "Hi there!"}
```
-This will attempt to match a route with the request map and return the matched route' response. If no route was found, it will attempt to find a route that has a `:path` that is `:not-found`, and return its response instead. But if not even that route was found, it will simply return a built-in 404 response instead.
+This will attempt to match the best route for the request map and return its response. Routes are matched using **best-match semantics** — the most specific route always wins, regardless of the order routes appear in the vector. If no route was found, it will attempt to find a route that has a `:path` that is `:not-found`, and return its response instead. But if not even that route was found, it will simply return a built-in 404 response instead.
Note that the `request-method` doesn't have to be a keyword, it can be anything that your HTTP server returns. But it does have to be called `request-method` for the router to know where to look for. That said, you do not have to provide neither `method` in the route, nor `request-method` in the request if you don't want to. You can skip both of them and let Ruuter route based on the `:uri` alone if you want.
@@ -85,7 +91,9 @@ You can also use Ruuter with [Babashka](https://github.com/babashka/babashka), b
```clojure
#!/usr/bin/env bb
-(deps/add-deps '{:deps {org.clojars.askonomm/ruuter {:mvn/version "1.3.5"}}})
+(deps/add-deps '{:deps {askonomm/ruuter {:git/url "https://git.nmm.ee/asko/ruuter.git"
+ :git/tag "v2.0.0"
+ :git/sha "<sha>"}}})
(require '[org.httpkit.server :as http]
'[babashka.deps :as deps]
@@ -103,7 +111,7 @@ You can also use Ruuter with [Babashka](https://github.com/babashka/babashka), b
### Creating routes
-Like mentioned above, each route is a map inside a vector - the order is important only in that the route matcher will return the first result it finds according to `:path`.
+Like mentioned above, each route is a map inside a vector. Routes are matched using **best-match semantics** — the most specific route wins regardless of order.
Each route consists of three items:
@@ -115,7 +123,7 @@ To create parameters from the path, prepend a colon (:) in front of a path slice
##### Required parameters
-A required parameter with a string such as `/hi/:name`, which would match any string that matches the `\/hi\/.*` regex in the URI, in its own slice. The `:name` itself will then be available with its value from the `request` passed to the response function, like this:
+A required parameter with a string such as `/hi/:name`, which would match any string in its own slice. The `:name` itself will then be available with its value from the `request` passed to the response function, like this:
```clojure
(fn [req]
@@ -126,7 +134,7 @@ A required parameter with a string such as `/hi/:name`, which would match any st
##### Optional parameters
-A optional parameter with a string such as `/hi/:name?`, which would match any string that matches the `\/hi\/?.*?` regex in the URI, in its own slice. If there is a `:name` provided in the URI then it will then be available with its value from the `request` passed to the response function, like this:
+An optional parameter with a string such as `/hi/:name?`, which would match any string in its own slice, but is not required to be present. If there is a `:name` provided in the URI then it will then be available with its value from the `request` passed to the response function, like this:
```clojure
(fn [req]
@@ -137,7 +145,7 @@ A optional parameter with a string such as `/hi/:name?`, which would match any s
##### Wildcard parameters
-The above-mentioned `:name` and `:name?` only match in its own path slice, e.g inside a space surrounded by two forward slashes. They cannot, by design, match the whole URL path. If you need wildcard matching, instead use `:name*`, which will match everything, including forward slashes.
+The above-mentioned `:name` and `:name?` only match in their own path slice, e.g inside a space surrounded by two forward slashes. They cannot, by design, match the whole URL path. If you need wildcard matching, instead use `:name*`, which will match everything including forward slashes. A wildcard parameter must be the last segment in a path.
#### `:method`
@@ -172,54 +180,72 @@ Or a function returning a map:
What the actual map can contain that you return depends again on the HTTP server you decided to use Ruuter with. The examples I've noted here are based on [http-kit](https://github.com/http-kit/http-kit) & [ring + jetty](https://github.com/ring-clojure/ring), but feel free to make a PR with additions for other HTTP servers.
-## Changelog
+### How It Works
-### 1.3.5
+Under the hood, Ruuter compiles your route definitions into a **segment trie** (prefix tree). Each segment of a path becomes a node in the tree, with branches for literal strings, parameters, optional parameters, and wildcards. This means route matching runs in O(path-depth) time — proportional to the number of segments in the URI, not the number of routes — so performance stays constant whether you have 5 routes or 5,000.
-- Fixes an issue where the usage of wildcard parameters [did not work quite right](https://github.com/askonomm/ruuter/issues/7).
-- Added test cases to cover the fix
+When a request comes in, the trie is walked depth-first, trying all branches at each node in **specificity order** and tracking the best match found so far:
-### 1.3.4
+| Priority | Segment type | Score | Example |
+|----------|-------------|-------|---------|
+| 1st | Literal | +3 | `users` |
+| 2nd | Required param | +2 | `:id` |
+| 3rd | Optional param | +1 | `:id?` |
+| 4th | Wildcard | +0 | `:path*` |
-- Fixes an issue where if used with middlewares (like Ring), or really anything that passes a `:params` key from outside of Ruuter in the request, Ruuter overwrites the `:params` key with its own parametarization. It now does a deep merge instead, with the outside parameters having priority. This means that Ruuter parameters will remain unless overwritten, and should co-exist with outside parameters nicely. [Issue #6](https://github.com/askonomm/ruuter/issues/6).
+The route with the highest total score wins. This means you can define routes in any order and always get the expected behavior:
-- Added and fixed some tests
-
-### 1.3.3
-
-- Removed ClojureScript from dependencies to make the bundle size smaller in case you want to use Ruuter with nbb.
+```clojure
+(def routes [{:path "/api/:resource" :method :get :response ...}
+ {:path "/api/users" :method :get :response ...} ; wins for /api/users
+ {:path "/api/users/:id" :method :get :response ...}
+ {:path "/api/users/me" :method :get :response ...} ; wins for /api/users/me
+ {:path "/:catch*" :method :get :response ...}])
+```
-### 1.3.2
+No regex is involved — matching is done via direct string comparison of path segments against the trie.
-- When using wildcard parameters, the keyword returned in ´:params´ of a request was ´:name*´, but aiming for consistency with an optional parameter where we remove the question mark ´?´, the asterisk has been removed.
+### Pre-Compiling Routes
-### 1.3.1
+When you pass a routes vector to `ruuter/route`, the trie is compiled automatically on first use and cached via `memoize`. For most applications this is all you need.
-- A small bugfix related to wildcard parameters losing the first character in the result.
+If you want explicit control — for instance, to compile once at startup, to avoid the memoization cache, or to inspect the compiled structure — use `compile-routes`:
-### 1.3.0
+```clojure
+(def compiled (ruuter/compile-routes routes))
-- Fixed an issue with optional parameters not matching correctly when there were multiple optional paremeters in use.
-- Implemented wildcard parameters in the form of `:name*`, which will match everything including forward slashes.
+;; Pass the pre-compiled trie to route — no compilation step at request time
+(ruuter/route compiled request)
+```
-### 1.2.2
+The return value of `compile-routes` is a map with `:trie` (the segment trie) and `:not-found` (the fallback route, if any), tagged with metadata so `route` can detect it and skip recompilation.
-- Fixed an issue where CLJS compilation would fail because of the `(:gen-class)` that is JVM-only.
+## Development
-- Tests are now runnable for CLJS as well, via `clojure -Atest`.
+Ruuter uses `deps.edn` (Clojure CLI) and `bb.edn` (Babashka) for all development tasks.
-### 1.2.1
+### Running Tests
-- Fixed an issue with regex parsing. Sorry about that.
+```bash
+# JVM (Clojure)
+clojure -M:test
-### 1.2.0
+# ClojureScript (Node.js)
+clojure -M:cljs-test
-- Implemented optional route parameters, so now you can do paths like `/hi/:name?` in your routes, and it would match the route even if the `:name` is not present. All you have to do is add a question mark to the parameter, and that's it.
+# Babashka
+bb test
+```
-- Changed Ruuter from a .clj file to a .cljc file, so it would also work with ClojureScript. Although it would probably require a more hands-on set-up than just a drop-in to an HTTP server like http-kit or ring + jetty, there is no reason that the router itself wouldn't work as it does not rely on any platform-specific code.
+### Running Benchmarks
-- Ruuter also works with [Babashka](https://github.com/babashka/babashka), and I've created a "Setting up with Babashka" section in this README to show that.
+```bash
+# JVM
+clojure -M:bench
-### 1.1.0
+# ClojureScript
+clojure -M:cljs-bench && node bench-out/bench.js
-- Made Ruuter server-agnostic, which means now it really is just a router and nothing else, and can thus be used with just about any HTTP server you can throw at it. It also means there are now zero dependencies! ZERO!
+# Babashka
+bb bench
+```