HomeServicesExpertiseOpen source supportAboutStoryReferencesCareerInsightsContact
Thursday, 27 February, 2025
Joel Kaasinen
Technology

Using Malli to encode query parameters in Reitit-Frontend

A missing link between Malli and Reitit-Frontend

Post cover image

Our friends at Biotz reached out to us one dark winter evening. They enjoyed using Reitit-Frontend with Malli for their frontend routing needs, but found that they always needed the same piece of boilerplate code in every project. We were happy to help them by adding a feature to Reitit under our Commercial Open Source Support program.

What the developers at Biotz wanted was to generate URLs like

http://my-app/page?filter=active&since=d2025-01-02

... based on a Reitit route definition with Malli schemas:

["/page"
 {:name ::item
  :view item-page
  :parameters {:query [:map
                       [:filter {:optional true} :keyword]
                       [:since {:optional true} ::instant]]

... and a map of the parameters:

{:filter :active
 :since #inst "2025-01-02T00:00:00.000-00:00"}

The current behaviour of Reitit was to just directly convert the parameters to strings, without using the Malli schema for the parameters to guide the encoding. This was a problem because the parameters were then decoded by the Malli schema when the user navigated to the url.

Making the query parameters round-trip via Malli encoders & decoders was an obvious missing piece in Reitit, so our frontend expert Juho quickly whipped up a prototype. After some further feedback from Biotz, he added more features, and the result was released as part of Reitit 0.8.0-alpha1.

Here's how it works. Consider a simple Reitit-Frontend router:

(ns example
  (:require [clojure.string :as str]
            [reitit.frontend :as rf]
            [reitit.coercion.malli :as rcm]
            [reitit.frontend.easy :as rfe]))

;; malli schema for vector of keywords that gets encoded as
;;   [:a :b :c] <=> "a_b_c"
(def vector-with-custom-encoding
  [:vector
   {:encode/string (fn [xs] (str/join "_" (map name xs)))
    :decode/string (fn [s] (mapv keyword (str/split s #"_")))}
   :keyword])

;; malli schema for a string that gets encoded as UPPER CASE
;; but decoded in lower case
(def uppercase-string
  [:string {:encode/string (fn [s] (str/upper-case s))
            :decode/string (fn [s] (str/lower-case s))}])

;; router, two paths, one with coercion and the other without
(def router
  (rf/router
   ["/"
    ["no-coercion"
     {:name ::no-coercion
      :parameters {:query [:map
                           [:color uppercase-string]
                           [:animals vector-with-custom-encoding]]}}]
    ["with-coercion"
     {:name ::with-coercion
      :coercion rcm/coercion
      :parameters {:query [:map
                           [:color uppercase-string]
                           [:animals vector-with-custom-encoding]]}}]]))

;; print the :name of the path and the paramerers when navigating
(defn on-navigate [match _history]
  (prn :NAVIGATE (-> match :data :name) (-> match :parameters)))
(rfe/start! router on-navigate {})

When using reitit.frontend.easy/href to generate a URL for a path with no coercion, we get the old, default behaviour. Vectors get transformed to repetitions of the same query parameter, and strings get used directly:

(rfe/href ::no-coercion
          {}
          {:color "green" :animals [:fox :hedgehog]})
;; => "#/no-coercion?color=green&animals=fox&animals=hedgehog"

But now, with the new feature, we get the right encoding for parameters for the endpoint that uses coercion:

(rfe/href ::with-coercion
          {}
          {:color "green" :animals [:fox :hedgehog]})
;; => "#/with-coercion?color=GREEN&animals=fox_hedgehog"

Note how the color is in upper-case and the animals query parameter is used only once.

If we now change the URL in the browser to #/with-coercion?color=GREEN&animals=fox_hedgehog, we'll see our on-navigate function print the decoded parameters, in exactly the same format as we gave them to the href function:

:NAVIGATE :example/with-coercion
{:query {:color "green", :animals [:fox :hedgehog]}}

Thanks once again to Biotz for sponsoring this feature! See the docs for more info.

Joel Kaasinen
Thursday, 27 February, 2025

Related posts

High-Level AI Strategy into a Prioritized Investment Roadmap

by Tapio Nissilä
Cover Image for High-Level AI Strategy into a Prioritized Investment Roadmap

Three Levels of AI Impact

by Tapio Nissilä
Cover Image for Three Levels of AI Impact

A Guide for Technology Executives

by Tapio Nissilä
Cover Image for A Guide for Technology Executives
More

Contact

HomeServicesExpertiseOpen source supportAboutStoryReferencesCareerInsightsContact

Tampere
Hämeenkatu 13 A 5
33100 Tampere

Helsinki
Kalevankatu 13, 3rd floor
00100 Helsinki

Jyväskylä
Väinönkatu 30, 5th floor
40100 Jyväskylä

Oulu
Kirkkokatu 4 A 51
90100 Oulu

Business-id: 3374640-7
E-invoice: 003733746407
Operator: 003723327487 / Apix
metosin@skannaus.apix.fi

first.last@metosin.fi

Github
Instagram
LinkedIn
YouTube

Cookie settings