August 10, 2020

My go-to Clojure libraries

I started developing in Clojure at the end of 2014, and I use it professionally since 2018. With time, I built a list of libraries which are for me essential when I work with Clojure. Some of these libraries were written at Exoscale, where I work. A big thanks to all maintainers and contributors ;)

Error handling

I always had troubles managing errors in Clojure until I discovered ex (written by a coworker, thanks to him :p). This library was a game changer for me.

ex allows you to define exception types in the ex data by setting the :exoscale.ex/type key. You can then use the try+ macro to catch Exceptions with a given type, like in this example:

(ex/try+

  (throw (ex-info "Argh" {:type ::bar :foo "a foo"}))

  (catch ::foo data
    (prn :got-ex-data data))

  (catch ::bar {:as data :keys [foo]}
    ;; in that case it would hit this one
    (prn :got-ex-data-again foo))

  (catch ExceptionInfo e
   ;; this would match an ex-info that didn't get a hit with catch-ex-info)

  (catch Exception e (prn :boring))

  (finally (prn :boring-too))

ex also provides default types like :exoscale.ex/not-found, :exoscale.ex/forbidden and helper functions to create exceptions (like (exoscale.ex/ex-not-found "not found !" {:foo "bar"}) to create an exception of type :exoscale.ex/not-found, or exoscale.ex/ex-not-found! to create and throw it).

ex also allows you to derive exceptions types from other types.
Let’s take an example. You might want have multiple types for authentication errors, like ::invalid-api-key or ::invalid-token, but make them all derive from ::ex/forbidden to be able to convert them easily to an HTTP response:

(ex/derive ::invalid-api-key ::ex/forbidden)
(ex/derive ::invalid-token ::ex/forbidden)

(defmulti ex->response
  (fn [e]
    (some-> e ex-data :type))
  :hierarchy exoscale.ex/hierarchy)

(defmethod ex->response ::ex/forbidden [e]
  {:body (ex-message e)
   :status 403})

(defmethod ex->response ::ex/not-found [e]
  {:body (ex-message e)
   :status 404})

(defmethod ex->response :default [_]
  {:body "default error message"
   :status 500})

Let’s generate some errors:

(ex->response (ex/ex-not-found "thing not found"))
{:body "thing not found", :status 404}

(ex->response (ex-info "invalid token" {:type ::invalid-token}))
{:body "invalid token", :status 403}

(ex->response (ex-info "invalid api key" {:type ::invalid-api-key}))
{:body "invalid api key", :status 403}

(ex->response (ex-info "error !" {}))
{:body "default error message", :status 500}

You can do tons of things with this library !

HTTP

I like Ring Jetty Adapter as a HTTP server. I use a lot Aleph at my job, but the current state of the project is not reassuring. I’ve recently started migrating my side projects to Jetty, and it works perfectly.
Performances are also great: one of my coworker used Ring Jetty to serve thousands of requests per seconds on commodity hardware. it seems everyone want reactive/async softwares today (which is possible with Jetty), but I think blocking is alright for most projects.

I don’t use ring middlewares because I prefer the Interceptor pattern. For that, I use the Interceptor library from Exoscale. This library also supports async interceptors (based on Manifold, core.async or CompletableFuture).

For me, it’s easier to reason about my request pipeline using interceptors rather than middlewares. It’s also easy to convert Ring middlewares to interceptors, for example for cookies:

(def cookies
  {:name ::cookies
   :enter (fn [ctx] (update ctx :request #(cookies/cookies-request % {})))
   :leave (fn [ctx] (update ctx :response #(cookies/cookies-response % {})))})

Here, I reuse functions from ring.middleware.cookies in my interceptor.

json

I always used cheshire and never had issues with it, so I will continue to use it.

HTTP client

clj-http is what you want.

Crypto

I like crypto-random to generate random bytes and crypto-password to manage passwords.

Configuration loading

Aero is a very good library if you want to load EDN configurations. For YAML, I use Yummy which supports tons of YAML tags (like !keyword, !envvar…​).

If I have to read things from environment variables, I use the environ library.

SSL

Less-awful-ssl is mandatory to create SSL contexts from certificates files.

SQL

I use next-jdbc to access SQL databases. The API is nice and it’s easy to use.

For database migrations, I use Ragtime. Again, it’s easy to use and I never had issues with it.

I use HikariCP for my databases connection pools. It’s a Java library but it’s easy to use from Clojure. Here is an example for Postgresql:

(defn pool
  [{:keys [user password host port name max-pool-size key cert cacert ssl-password ssl-mode schema]}]
  (let [url (format "jdbc:postgresql://%s:%d/%s"
                    host port name)
        config (doto (HikariConfig.)
                 (.setMetricRegistry metric/registry)
                 (.setJdbcUrl url)
                 (.addDataSourceProperty "user" user)
                 (.addDataSourceProperty "password" password)
                 (.setMaximumPoolSize (or max-pool-size default-pool-size)))]
    (when schema
        (.addDataSourceProperty config "currentSchema" schema))
    (when key
      (.addDataSourceProperty config "ssl" true)
      (.addDataSourceProperty config
                              "sslfactory"
                              "org.postgresql.ssl.jdbc4.LibPQFactory")
      (.addDataSourceProperty config "sslcert" cert)
      (.addDataSourceProperty config "sslkey" key)
      (.addDataSourceProperty config "sslrootcert" cacert)
      (.addDataSourceProperty config "sslmode" (or ssl-mode
                                                   default-ssl-mode)))
    (HikariDataSource. config)))

EQL

EQL EDN Query Language is a language to query data using EDN datastructures. The seql library takes inspiration from EQL and can be used to access (and mutate) entities stored on SQL databases.

The documentation could be improved (I plan to write an article explaining how seql works in details), but you can check the quickstart for basic examples.

It supports for example listeners (functions which will be executed when a mutation is successfully executed), preconditions (when a mutation is performed, you can add checks to verify the database state. Preconditions are executed in the same transaction than the mutation).
t will also automatically verify (using clojure spec) if the parameters passed to mutations are valid or not, and even automatically coerce parameters (like string to uuid, string to keywords…​) to the right type !

Everything (the database schema, the mutations, the queries to execute …​) is represented as EDN.

We use seql intensively at Exoscale. it’s a powerful library which I really like to use.

Logging

I use tools.logging to log things, and unilog to configure the logger. Unilog is simple to use and allows you to choose the logging format (json for example), log things in files, control the log level of your loggers…​
Unilog also allows you to add a context to your log (new keys if the logs are json formatted for example) by using the unilog.context/with-context macro.

Metrics

I think the best solution today is to wrap the Java Micrometer library. It’s easy to use, supports tags, has tons of outputs (Prometheus, Graphite, Datadog…​). It’s just what I want from a metric library.

Tests

I use the default clojure.test runner, but for me humane-test-output to pretty print test outputs is mandatory. Here is an example without and with humane-test-output:

(deftest foo-test
  (is (= {:foo 1 :bar 2}
         {:baz 3})))

;; without

expected: (= {:foo 1, :bar 2} {:baz 3})

  actual: (not (= {:foo 1, :bar 2} {:baz 3}))

;; with

expected: {:foo 1, :bar 2}

  actual: {:baz 3}
    diff: - {:foo 1, :bar 2}
          + {:baz 3}

For mocks, the spy library allows you to easily mock protocols, functions…​ and check how many time things were called and with which parameters. I always include this library in my :dev profile.

Linter

clj-kondo is amazing, and brings me joy.

And you, what are your favorite libraries ?

Tags: programming english

Add a comment








If you have a bug/issue with the commenting system, please send me an email (my email is in the "About" section).

Top of page