How I developed a web backend in Clojure

marciofrayze

Marcio Frayze

Posted on July 6, 2023

How I developed a web backend in Clojure

One of my students from the course Clojure: Introduction to Functional Programming (Brazilian Portuguese only) asked an interesting question:

Hello teacher, I would like to know about the deployment of an application, with a frontend sending requests and getting responses (as in the case of the shopping cart, for example). I'm enjoying the course, but I think a minimal application with a front-end or a service implementation would bring us closer to "real world" usage.

Although the question is quite outside the scope of the course (which focuses on the concepts of the functional paradigm - the language is just a detail), it is understandable. It is natural to want to know how to apply the new concepts in practice.

I answered the student, but decided to write this article so I could give a better and more complete response. I will not show how to create a REST service in Clojure step by step, but will give some possible paths, aiming to facilitate the journey of those interested in this topic.

Throughout this article I show what was my train of thought during the implementation of a REST backend in Clojure that I started developing last year and that I continue to support and add features to this day.

Single Page Application or Server Side?

One of the decisions I had to make early on was whether to render the HTML on the backend and use a fullstack Clojure framework or whether to implement a SPA, using Clojure only on the backend (through REST services).

Clojure supports both approaches. And when choosing to develop a SPA, you can even use Clojure to implement the frontend as well, using ClojureScript!

But I chose to use Clojure only on the backend and developed the frontend using my favorite programming language: Elm.

I wanted to use only functional languages ​​in my stack and since (at least for now) Elm is focused on frontend development, I needed another language for the backend. As I already wanted to explore the use of the Elm Land framework and the Elm UI library, I chose to create a SPA in Elm and use Clojure only on the backend.

But if you want to, it is possible to implement the whole solution in Clojure.

Frameworks or libraries?

In many languages ​​it is common to have web frameworks that includes various functionalities. Anyone who has ever programmed in Java has probably studied Spring or Quarkus. In Ruby we have the famous Ruby on Rails, in PHP there are solutions like Laravel and Symfony (or even the infamous Wordpress), and many others. But there are also many libraries.

It's hard to segment and define what makes a piece of technology to be a framework or a library, and I won't try to do that here. For this article, just consider that while a framework tries to solve many different types of problems, a library tends to have a much smaller scope. Thus, in general, it is necessary to compose a set of libraries to solve a larger problem.

As examples of libraries, in Ruby I could mention Sinatra, for Java/Kotlin Spark, and many others.

And as you might guess, there are also frameworks and libraries for web development in Clojure. And one famous framework is Luminus.

Luminus

Luminus defines itself as a micro-framework based on a set of lightweight libraries. This is a relatively common feature among web frameworks: putting together several small libraries in a cohesive and well-integrated way that, together, have everything usually needed to implement a complete web solution.

I recommend learning the basics of Luminus to see what is possible to do in Clojure. With it, you can easily create a well-organized project with all the features currently expected in a modern web framework, including a migrations system for the database schema, automated tests, environment-specific configurations, some helpers to interact with the database, etc.

But, while very interesting, it seems to me that this approach is not popular within the Clojure community. What I have observed is that, in general, Clojure developers tend to prefer to choose some libraries and build their own framework, according to their needs.

And this was also the path I chose to follow: creating my own customized framework for the needs of my project. In part, I followed this path because what I needed to build was not something too complex. So I took the opportunity to venture into the various libraries of the Clojure universe!

Selecting the libraries

Automation and dependency management

I chose Leiningen to manage project dependencies and automate tasks like running automated tests, generating the project build (Uberjar), running the REPL terminal (Read-Eval-Print Loop), etc.

If you come from the Java world, think of Leiningen as an alternative to Apache Maven or Gradle.

There are other similar tools, but Leiningen seems to be the most used by the Clojure community and it works fine.

HTTP server

To develop a web application it is necessary, of course, to have a server to receive the requests. For this, I chose the Ring library.

For those familiar with Python, you might think that Ring would be something equivalent to WSGI (Web Server Gateway Interface) and developers used to Ruby might associate it with Rack.

Ring is simple but very low-level and can be tricky to set up. I chose to use the Ring-Defaults library, which aims to provide a secure initial configuration that meets most cases. This way it was very easy to configure Ring and create my first REST service in Clojure.

Route management

Ring is so slim that it doesn't even have a route management system! So I needed to add a library for this purpose.

For this, I chose to use Compojure. It is a very simple library, which adds some functionalities to Ring related to routes creation.

Creating a /hello-world endpoint using Compojure is straightforward:

(defroutes app
  (GET "/hello-world" [] "<h1>Hello World</h1>"))

Enter fullscreen mode Exit fullscreen mode

For complex services you can (and should) delegate the processing of the requests to other functions.

Connecting to the database (PostgreSQL)

With the libraries listed above, it is possible to create a simple server and reply the requests. But most projects will need more than that.

A common need is to communicate with a database. In my case, I needed to access a PostgreSQL database.

I chose to use the clojure.java.jdbc library. But note that although this library is still maintained (the author continues to apply bug fixes and minor releases), it is described on the website as being "Stable" (no longer "Active") and they recommend using next-jdbc instead.

Therefore, in a new project, I would give priority to this other library (and it is in my plans to migrate the project to it in the future).

PostgreSQL

The clojure.java.jdbc library is agnostic and can be used on any database with JDBC support (Java Database Connectivity). To connect to a PostgreSQL database, I also needed to include org.postgresql/postgresql.

Connection Pooling

The two libraries I mentioned above are enough to connect and execute queries in the database but, for performance reasons, it is important to use a pool of connections. For that I use and recommend hikari-cp. I already used it when programming in Java and it always worked fine.

Creating the queries

Developers familiar with object-oriented programming languages (such as Java and C#) should be used to ORM (Object Relational Mapper) tools like Hibernate or Entity Framework. But within the Clojure community, I don't see this type of tool being used much. In the context of Clojure, it is more natural for a database call to return a data structure, such as a map.

I chose to write the SQL code manually in a few complex queries, but for most cases I use the Honey SQL library. With it, you can build queries as if they were Clojure data structures.

An example would be:

(defn query-select-audit-xpto-by-user-id
  [user-id]
  (-> (h/select :created_at
                :remote_address
                [[:raw "request_headers->'x-alias'"] :alias])
      (h/from :audit)
      (h/where [:= :user_id user-id]
               [:= :path "/api/xpto/"]
               [:= :response_status 200]
               [:= :method "GET"])
      (h/order-by [:id :desc])
      sql/format))

Enter fullscreen mode Exit fullscreen mode

Note that this function does not execute the query, it just generates the SQL command, therefore it is a pure function. When you run it, the result will be:

(query-select-audit-xpto-by-user-id "123.456.789-01")

;; Result:
["SELECT created_at, remote_address, request_headers->'x-alias' AS alias FROM audit WHERE (user_id = ?) AND (path = ?) AND (response_status = ?) AND (method = ?) ORDER BY id DESC" "123.456.789-01" "/api/xpto/" 200 "GET"]

Enter fullscreen mode Exit fullscreen mode

To actually process it, you have to call the jdbc/with-db-connection function from the clojure.java.jdbc library, passing the query as a parameter. Before that, of course, you must configure the user, password and address of the database, in addition to the connection pool configuration. But that is beyond the scope of this article.

JSON

JSON is used very often in web projects. To facilitate the parsing (and generation) of this format, I use the data.json library.

Its use is quite simple:

(json/write-str {:a 1 :b 2})
;; Result:
"{\"a\":1,\"b\":2}"

(json/read-str "{\"a\":1,\"b\":2}")
;; Result:
{"a" 1, "b" 2}

Enter fullscreen mode Exit fullscreen mode

Performing HTTP calls

In addition to receiving HTTP requests, I needed to integrate with other systems through REST services (another very common need in distributed web applications).

For that I used the library clj-http and its use is quite simple. To make a GET call, just run something like:

(client/get "https://example.com/resource/1234" {:accept :json})

Enter fullscreen mode Exit fullscreen mode

Caching

The system I developed executes a few heavy queries in a large database, to generate some reports. But while those processing are costly, the end result is not a huge amount of data and it doesn't need to be fully up-to-date all the time. Therefore, some of these operations may be cached in memory.

For that, I used the clojure.core.cache library.

Although this library works well, I found its interface a bit confusing. To work around this I created my own abstraction. After some tweaks, I managed to create some functions that, although they hide some of the more advanced library functionalities, make the creation of caches quite easy.

Asynchronous calls

At a certain point I needed to integrate with the Microsoft Teams chat (through an HTTP call). As I mentioned above, I use clj-http for this. But I wanted this part of the system to be asynchronous (non-blocking). For this I use the core.async library.

For those familiar with the Go programming language, this library works similarly to the famous Goroutines. That is, it is a lightweight thread.

Turning a blocking function into non-blocking is very easy: just pass the expressions/functions you want to run in parallel to the async/go function. In the case of an HTTP call, it would be something like:

(async/go
  (try
    (client/post webhook-teams-url
                 {:body (json/write-str {"text" "A message to MS Teams"})
                  :headers {"content-type" "application/json"}
                  :socket-timeout 3000
                  :connection-timeout 3000})
    (catch Exception e
      (println (str "Failed to send message to Teams: " e)))

Enter fullscreen mode Exit fullscreen mode

Scheduler

Another common need for a web application, and which I also needed to do, is scheduling the execution of tasks that must be executed periodically (in my case, executing a function every 10 minutes). For this purpose, I chose to use the Chime library.

Although it is relatively simple, as with the cache library, I initially found it hard to understand how to use it. It's nothing too complex, but I found the documentation a bit confusing. Again I was able to isolate this part of the code in a more specific function and it worked. I had no more problems with it.

Mocks and automated tests

I'm a fan of TDD, so I also wanted to create automated tests.

In the beginning I chose to implement, in addition to the most basic unit tests, several integrated tests. For that I used the Ring-Mock library to mock the web server. I also created a test double for the function that accessed the database.

Over time I began to have trouble maintaining these tests, which broke in non-trivial ways. Another big problem was that when trying to practice TDD I missed a type system to create the asserts.

Sometimes I created the entire end-to-end scenario, but at runtime, when I was running the application and connecting to the real database, the returned type was different from what I was imagining. And again, it was frustrating at times to pinpoint exactly where the problem lay. The error messages were not always meaningful.

Over time I chose to minimize (at least for now) this type of test. I try to maximize the amount of pure functions and focus my tests on them. I miss testing some layers of the application, which is something I still need to experiment and explore further.

Isolating the pure functions

I don't want to go into too much detail about the software architecture in this article, but it's pretty straightforward. Each use case (service) has its own folder, and the pure and impure functions are in different namespaces. I strive to transform impure functions into pure ones and make those impure ones into orchestrators.

Functions that cause some kind of side effect should be as "dumb" as possible. For example: a function that executes a query in the database cannot have any business logic. Same for a function that performs an HTTP call, etc.

I already considered this to be a good practice when programming in other languages, but in Clojure (due to its functional-first approach), it becomes effortless and natural to do so.

Over time, I ended up decreasing the number and even eliminating some integrated tests and increasing the amount of unit tests. For my context, this worked fine, but sometimes I still miss broader tests.

Dependency injection

Developers used to languages like Java or C# might search for a dependency injection library so they can practice inversion of control.

To achieve this goal in Clojure, all I've needed so far is to pass function references as parameters. If a function needs, for example, to execute a query in the database, I pass as a parameter the reference to the function that, when receiving an SQL command, executes it and returns the result. I do the same for HTTP calls or other forms of side effects.

In the context of my application, this was enough. If I need to create a test double (mock/spy/stub), I just pass the reference of another function as a parameter, avoiding coupling with external layers (such as a database or other systems) in the tests and giving full control over the return in these calls, allowing me to write asserts that won't break over time. This way my tests can also run concurrently, in addition to being very fast.

Hosting

One of the great advantages of Clojure - and one of the main reasons I chose it - is the fact that it is a language that runs on the JVM (Java Virtual Machine). The end result is an Uberjar (a jar file with -standalone.jar suffix) containing all the application and project dependencies.

And it is possible to start the server with the following command line:

java -jar target/project-name-0.0.1-SNAPSHOT-standalone.jar

Enter fullscreen mode Exit fullscreen mode

In my case, I'm using an on-premise private cloud. And an interesting point is that this cloud already supported the Java platform and I had already developed many solutions in this language. Migrating to Clojure was trivial, not having to make any infrastructure changes. For the operations team, it's completely transparent.

So applications developed the way I described in this article can be hosted on any server that supports Java! 🎉

Bonus: Updating dependencies

A concern I always have in every project is to avoid rusting. It is a natural process in every application, so it needs constant attention. And at least for some of these types of problems, the process can be automated!

Leiningen has a command that checks for outdated dependencies and, if it finds any, is able to update them and then run the automated tests. If any tests fail, the change is undone and an error message is displayed indicating the problem.

This feature helps me daily! I usually start my day running it to make sure the project's dependencies are always up to date.

The command in question is:

lein ancient upgrade :interactive :check-clojure

Enter fullscreen mode Exit fullscreen mode

But to be able to run it, you need to configure the lein-ancient plugin.

My current project.clj

All Leiningen configuration is done in the project.clj file.

Below I share how my project is set up at the time I write this article. In addition to the libraries I've presented here, I use a few other settings that I didn't find relevant enough to highlight, but that might be useful for some people.

(defproject project-name "1.0.0-SNAPSHOT"
  :description "Here would be the description of the project."
  :url "https://git-repository-address"
  :min-lein-version "2.10.0"
  :dependencies [[org.clojure/clojure "1.11.1"]
                 [compose "1.7.0"]
                 [ring/ring-defaults "0.3.4"]
                 [com.github.seancorfield/honeysql "2.4.1045"]
                 [org.clojure/java.jdbc "0.7.12"]
                 [org.postgresql/postgresql "42.6.0"]
                 [hikari-cp "3.0.1"]
                 [org.clojure/data.json "2.4.0"]
                 [org.slf4j/slf4j-simple "2.0.7"]
                 [clj-http "3.12.3"]
                 [org.clojure/core.cache "1.0.225"]
                 [jarohen/chime "0.3.3"]
                 [org.clojure/core.async "1.6.673"]]
  :plugins [[lein-ring "0.12.6"]
            [lein-ancient "0.7.0"]
            [lein-cloverage "1.2.2"]]
  :ring {:handler a.namespace.routes/app}
  :repl-options {:init-ns a.namespace.whatever.logic}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [ring/ring-mock "0.4.0"]]}})

Enter fullscreen mode Exit fullscreen mode

Conclusions

Clojure, plus all the libraries that I showed throughout this article, served me very well. I managed to create an architecture that is both simple and easy to add new services. The server consumes few resources and has a very good performance.

Choosing to create my own framework, selecting each library separately, initially took some time. I had to test each one of them, discard the ones that didn't meet my needs and create some code to "glue" these libraries together.

Using a full framework (like Luminus) would probably increase my productivity in the first few iterations, but since it's a project I hope to keep for many years, having full control over every part of it is probably the ideal scenario.

The Clojure community seems to have a culture of crafting small, well-defined purpose libraries. I believe that this characteristic, plus the fact that Clojure is a functional-first language, made this process much easier. These two features together allowed me to be able to create large abstractions by just using function composition, without adding too much complexity.

But it's important to make it clear that this is a small project, where I didn't need to use any fancy architecture. I didn't cover anything like Domain-driven design, Hexagonal Architecture, Microservices, ... So I can't say that Clojure (or even the functional paradigm) would be a good fit for complex scenarios. But so far, I'm not regretting the choice and, if it's up to me, I won't be programming in an object-oriented language again anytime soon! 😇


Did you like this article? Don't forget to leave a reaction! They serve as a big incentive for me to write others.

You can also discover more articles, podcasts and videos at: https://segunda.tech/tags/english.

And if you want, follow me on twitter and blue sky.

💖 💪 🙅 🚩
marciofrayze
Marcio Frayze

Posted on July 6, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related