# Ruuter A tiny, zero dependency, system-agnostic router for Clojure, ClojureScript, Babashka and NBB that operates with a simple data structure where each route is a map inside a vector. Yup, that's it. No magic, no bullshit. ## Installation 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 ""}}} ``` ## Usage ### Setting up Require the namespace `ruuter.core` and then pass your routes to the `route` function along with the current request map, like this: ```clojure (ns myapp.core (:require [ruuter.core :as ruuter])) (def routes [{:path "/" :method :get :response {:status 200 :body "Hi there!"}}]) (def request {:uri "/" :request-method :get}) (ruuter/route routes request) ; => {:status 200 ; :body "Hi there!"} ``` 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. ### Setting up with [http-kit](https://github.com/http-kit/http-kit) Now, obviously on its own the router is not very useful as it needs an actual HTTP server to return the responses to the world, so here's an example that uses [http-kit](https://github.com/http-kit/http-kit): ```clojure (ns myapp.core (:require [ruuter.core :as ruuter] [org.httpkit.server :as http])) (def routes [{:path "/" :method :get :response {:status 200 :body "Hi there!"}} {:path "/hello/:who" :method :get :response (fn [req] {:status 200 :body (str "Hello, " (:who (:params req)))})}]) (defn -main [] (http/run-server #(ruuter/route routes %) {:port 8080})) ``` ### Setting up with [Ring + Jetty](https://github.com/ring-clojure/ring) [Ring + Jetty](https://github.com/ring-clojure/ring) set-up is almost identical to the one of http-kit, and looks like this: ```clojure (ns myapp.core (:require [ruuter.core :as ruuter] [ring.adapter.jetty :as jetty])) (def routes [{:path "/" :method :get :response {:status 200 :body "Hi there!"}} {:path "/hello/:who" :method :get :response (fn [req] {:status 200 :body (str "Hello, " (:who (:params req)))})}]) (defn -main [] (jetty/run-jetty #(ruuter/route routes %) {:port 8080})) ``` ### Setting up with [Babashka](https://github.com/babashka/babashka) You can also use Ruuter with [Babashka](https://github.com/babashka/babashka), by using the built-in http-kit server, for example. Either add the dependency in your `bb.edn` file or if you want to make the whole thing one-file-rules-them-all, then load it in with `deps/add-deps`, like below: ```clojure #!/usr/bin/env bb (deps/add-deps '{:deps {askonomm/ruuter {:git/url "https://git.nmm.ee/asko/ruuter.git" :git/tag "v2.0.0" :git/sha ""}}}) (require '[org.httpkit.server :as http] '[babashka.deps :as deps] '[ruuter.core :as ruuter]) (def routes [{:path "/" :method :get :response {:status 200 :body "Hi there!"}}]) (http/run-server #(ruuter/route routes %) {:port 8082}) @(promise) ``` ### Creating routes 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: #### `:path` A string path starting with a forward slash describing the URL path to match. To create parameters from the path, prepend a colon (:) in front of a path slice like you would with a Clojure keyword. ##### Required parameters 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] (let [name (:name (:params req))] {:status 200 :body (str "Hi, " name)})) ``` ##### Optional parameters 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] (let [name (:name (:params req))] {:status 200 :body (str "Hi, " name)})) ``` ##### Wildcard parameters 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` The HTTP method to listen for when matching the given path. This can be whatever the HTTP server uses. For example, if you're using http-kit for the HTTP server then the accepted values are: - `:get` - `:post` - `:put` - `:delete` - `:head` - `:options` - `:patch` #### `:response` The response can be a direct map, or a function returning a map. In case of a function, you will also get passed to you the `request` map that the HTTP server returns, with added-in `:params` that contain the values for the URL parameters you use in your route's `:path`. Thus, a `:response` can be a map: ```clojure {:status 200 :body "Hi there!"} ``` Or a function returning a map: ```clojure (fn [req] {:status 200 :body "Hi there!"}) ``` 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. ### How It Works 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. 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: | Priority | Segment type | Score | Example | |----------|-------------|-------|---------| | 1st | Literal | +3 | `users` | | 2nd | Required param | +2 | `:id` | | 3rd | Optional param | +1 | `:id?` | | 4th | Wildcard | +0 | `:path*` | The route with the highest total score wins. This means you can define routes in any order and always get the expected behavior: ```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 ...}]) ``` No regex is involved — matching is done via direct string comparison of path segments against the trie. ### Pre-Compiling Routes 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. 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`: ```clojure (def compiled (ruuter/compile-routes routes)) ;; Pass the pre-compiled trie to route — no compilation step at request time (ruuter/route compiled request) ``` 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. ## Development Ruuter uses `deps.edn` (Clojure CLI) and `bb.edn` (Babashka) for all development tasks. ### Running Tests ```bash # JVM (Clojure) clojure -M:test # ClojureScript (Node.js) clojure -M:cljs-test # Babashka bb test ``` ### Running Benchmarks ```bash # JVM clojure -M:bench # ClojureScript clojure -M:cljs-bench && node bench-out/bench.js # Babashka bb bench ```