Metosin

Faster and Friendlier Routing with Reitit 0.3.0

We are happy to introduce a new version of reitit, a fast new data-driven routing library for Clojure/Script!

[metosin/reitit "0.3.0"]

The version is mostly backwards compatible, but contains a lot of big features, including a new route syntax and a new error formatter. As the last post was about version 0.2.0, here's a quick tour of the major changes since that.

New Route Syntax

Before 0.3.0, only colon-based path-parameters were supported. Parameters had to fill the whole space between slashes:

[["/users/:user-id"]
 ["/api/:version/ping"]]

The syntax is simple, but it doesn't allow the use of qualified path-parameters. The new wildcard routing in 0.3.0 support both the old syntax and a new bracket-syntax:

[["/users/{user-id}"]
 ["/api/{version}/ping"]]

Qualified keywords can be used, and parameters don't have to span whole segments between slashes:

[["/users/{domain/user-id}"]
 ["/files/file-{domain.file/number}.pdf"]]

More details in the route syntax documentation.

On Error

There has been a lot of complaints about error messages in Clojure. With Clojure 1.10.0, things are bit better, but the default error printing is still far from the friendly error messages of Elm and Eta.

In reitit, router creation time error messages have been rethought in 0.3.0. In case of error, an ExceptionInfo is thrown with a qualified error name and ex-data having all the relevant data. reitit.core/router catches all Exceptions and rethrows them with an enhanced error message, done by a configured error formatter.

The default formatter formats errors just like before. For more friendly errors, there is a new module reitit-dev, which contains an error message formatter based on fipp and expound (and the lovely 8bit colors from rebl-readline).

Below are few sample error messages.

On Route Conflict

(require '[reitit.core :as r])
(require '[reitit.dev.pretty :as pretty])

(r/router
  [["/ping"]
   ["/:user-id/orders"]
   ["/bulk/:bulk-id"]
   ["/public/*path"]
   ["/:version/status"]]
  {:exception pretty/exception})

Invalid Route Data

(require '[reitit.spec :as spec])
(require '[clojure.spec.alpha :as s])

(s/def ::role #{:admin :user})
(s/def ::roles (s/coll-of ::role :into #{}))

(r/router
  ["/api/admin" {::roles #{:adminz}}]
  {:validate spec/validate
   :exception pretty/exception})

The error formatter is developed and tested on macOS and currently only supports a dark theme. There have been discussion in #clj-commons Slack whether there could be a community-driven error formatter.

Performance

For routes with path-parameters, version 0.2.0 used a segment trie written in Clojure. It was already one of the fastest routers for Clojure, but still much slower compared to fast routers in other languages.

If search for better performance, the segment trie was ported into Java yielding 2x better performance. This was already good, but we didn't want to stop there.

For 0.3.0, the wildcard routing trie was fully rewritten, both with Clojure/Script and with Java. We used Profilers and flamegraphs (via clj-async-profiler) to find and eliminate the performance bottlenecks. On JVM, the trie is now 3x faster than the previous version, making it 6x faster than the one in 0.2.0.

I'll be talking about reitit in Clojure/North and will walk through the performance journey there in more detail.

According to the perf tests, reitit is now orders of magnitude faster than the other tested routing libraries and only less than twice as slow as httprouter, fast(est) router in Go. So, still some work to do ;)

The ClojureScript version is not as highly optimized as the Java version. If you have skills in tuning ClojureScript/JavaScript performance, feel free to contribute.

Spec Coercion

Spec coercion is now much more complete and works with plain clojure.spec Specs. Most of the changes have happened in spec-tools, which now also has a proper coercion guide. It currently supports only Spec1. There are also coercion guides on reitit side, for both core routing and for http/ring.

(require '[clojure.spec.alpha :as s])

(s/def ::x int?)
(s/def ::y int?)
(s/def ::total int?)
(s/def ::request (s/keys :req-un [::x ::y]))
(s/def ::response (s/keys :req-un [::total]))

(def math-routes
  ["/math"
   {:swagger {:tags ["math"]}}

   ["/plus"
    {:get {:summary "plus with query parameters"
           :parameters {:query ::request}
           :responses {200 {:body ::response}}
           :handler (fn [request]
                      (let [x (-> request :parameters :query :x)
                            y (-> request :parameters :query :y)]
                        {:status 200
                         :body {:total (+ x y)}}))}
     :post {:summary "plus with body parameters"
            :parameters {:body ::request}
            :responses {200 {:body ::response}}
            :handler (fn [request]
                       (let [x (-> request :parameters :body :x)
                             y (-> request :parameters :body :y)]
                         {:status 200
                          :body {:total (+ x y)}}))}}]])

See full example apps with spec coercion (and api-docs) for reitit-ring, reitit-http, reitit-frontend and reitit-pedestal.

Frontend

Small improvements, including a new polished api for controllers and HTML5 History routing works now with IE11. Both the Reagent template and kee-frame now default to reitit, which is really awesome.

Pedestal

Support for Pedestal was shipped already in 0.2.10 via new reitit-pedestal module. It allows the default Pedestal router to be swapped into reitit-http router. See the official documentation and an example app.

Final Words

0.3.0 was a big release, thanks to all contributors and pilot users! The full list of changes is found in the Changelog. We'll continue to actively develop the core libraries and the ecosystem around it, the roadmap is mostly laid out as Github Issues. Many issues are marked with help-wanted and good-first-pr, contributions are most welcome.

To discuss about reitit or to get help, there is a #reitit channel in the Clojurians Slack.

And last, but not least, I'll be giving a talk about reitit in Clojure/North on 20.4.2019. Looking forward to it.