Boring Clojure - Routing with Ring

Posted on July 11, 2020
Tags: clojure, programming, web, routing

Hi,

Recently I starting to think about trying to make a series of posts called ‘Boring Clojure’. My main goal here is to try to put together commons pieces of code that we always find in every web software, things like routing, rendering HTML, authentication, authorization, etc. As a first post, I would like to discuss Routing using Ring library.

The main reason to start with Ring, instead of Compujure, for instance, is the Ring is a low abstraction of HTTP details, which give us a better view on how things works, and also will provide us some basics fundaments that will be very useful when moving to a more high-level routing library like Compojure

Ring abstract HTTP details into a simple, unified API, Ring is inspired by Python’s WSGI and Ruby’s Rack. The Ring library is divided into four other libraries:

New Project

Lets starting exploring Ring by creating a new Clojure project and add it as a dependency.

lein new boring-clj

Open the project.clj file and add the ring as a dependency, our file should look like this:

(defproject boring-clj "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [ring "1.8.1"] ;; includes all ring libraries, like described above
                 ]
)

To download the dependencies, run lein deps. All setup, we are ready to go.

Ring as the basis for your web application

We have some nice benefits by choose Ring as routing library, such:

(source: https://github.com/ring-clojure/ring/wiki/Why-Use-Ring%3F)

Ring is the basement for much more high-level routing libraries, such Compojure. Once my idea here is to start from basics and try to not confusing the reading in a lot of abstraction, let’s forget about Compojure, for now.

Core Concepts

The four components of a ring are:

Handler

Are functions that compute the response for a request made on an endpoint. Your application is defined in terms of handlers, they can be synchronous(most commons) and asynchronous.

The synchronous handler takes one argument (request), and as a function body, we define a map request representation, which Ring can translate into an HTTP response.

(defn site [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello Clojure, Hello Ring!"})

Let’s run this handle to see more concrete things. Add the site function to src/core.clj, your core.clj should looks like this:

(ns auth-01.core
  (:require
   [ring.adapter.jetty :as jetty]))

(defn site [request]
  {:status 200
   :headers {"Content-Type" "text/plain"}
   :body "Hello Ring!"})

(defn -main
  [& [port]]
  (if port
    (jetty/run-jetty #'site {:port (Integer. port)})
    (println "No port specified, exiting.")))

We need one last update to run the application, open the project.clj and edit it to looks like it:

(defproject boring-clj "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "EPL-2.0 OR GPL-2.0-or-later WITH Classpath-exception-2.0"
            :url "https://www.eclipse.org/legal/epl-2.0/"}
  :dependencies [[org.clojure/clojure "1.10.1"]
                 [ring "1.8.1"]]

  :main boring.clj.core
  :ring {:handler boring-clj.core/site
         :init boring-clj.core/init}
  :repl-options {:init-ns boring-clj.core})

NOTE 1: Note the -main function is the entry point to our app. The - in the front of the name does not have any effect on the function behavior, is just a convention.

Now just execute lein run 3000 and access http://localhost:3001/

Request

So, we already see that HTTP requests are plain Clojure maps, in a ‘standard’ request, we will always have some keys present, which are:

As we will see next, there some keys that are added by Middlewares as well.

Response

Before we see that responses are created by handlers, and also are represented by Clojure in the form of:

{:status 200
 :headers {"Content-Type" "text/plain"}
 :body "Hello Clojure, Hello Ring!"})

Let’s see key by key:

Middleware

As told above, we can add functionality to handlers using Middlewares. Middleware is higher-level functions that allow us to add additional functionality to handlers:

For instance, let’s add the content type to the response:

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

As you can see, the first argument of a middleware function is the handle itself, and the rest is anything you middleware function need to do the job. The return should be a new handler function that will call the original handler. Let’s put this in the core.clj file and test to see if it works as expected:


;; site function now uses the `wrap-content-type`
;; Middle are to include the content-type in the HTTP response
(defn site [request]
  (wrap-content-type (fn [] {:status 200
                             :body "Hello Ring!"})))

(defn wrap-content-type [handler content-type]
  (fn [request]
    (let [response (handler request)]
      (assoc-in response [:headers "Content-Type"] content-type))))

We can test it in curl:

curl -s -I http://localhost:3000/
HTTP/1.1 200 OK
Date: Wed, 01 Jul 2020 13:30:00 GMT
Content-Type: text/plain
Transfer-Encoding: chunked
Server: Jetty(9.4.28.v20200408)

There are some conventions regarding functions names while play with middlewares: if the middleware function name is, for instance, wrap-content-type the helpers function to operate on the requests/response should be called content-type-response and content-type-request, so in our case, we need something like:

(defn site [request]
  {:status 200
   :body "<h1>Hello Ring</h1>"})

(defn content-type-response [response content-type]
  (assoc-in response [:headers "Content-Type"] content-type))


(defn wrap-content-type [handler content-type]
  (fn
    ([request]
     (-> (handler request) (content-type-response content-type)))))

(def my-app
  (wrap-content-type site "text/html"))

(defn -main
  [& [port]]
  (if port
    (jetty/run-jetty #'my-app {:port (Integer. port)})
    (println "No port specified, exiting.")))

Run the server and you should see some nice and beautiful HTML rendered.

Responses

Of course, to build the most basic app, we need a response too. As we see previous, a response is plain Clojure Maps and we can build it easily, but Ring also provides to us some util function to make the life even easier, let’s play in the REPL:

(use '[ring.util.response :as

auth-01.core> (response "Hello Ring" )
;; {:status 200, :headers {}, :body "Hello Ring"}

(content-type (response "Hello Ring") "text/plain")
;; {:status 200, :headers {"Content-Type" "text/plain"}, :body "Hello World"}

There some more utils function that you can check here. One interesting function that we will use later, is file-response, in order to static files and other resources (like CSS). ### Static resources

As told before, web applications also need some static resources, like CSSs, images, etc. With Ring we have some utility functions to handle it.

Let’s use wrap-resource, creates a file called index.html inside of resources/public/, them lets update our code to:

(ns auth-01.core
  (:require
   [ring.adapter.jetty :as jetty]
   [ring.util.response :as res])
  (:use [ring.middleware.resource]))

(defn site [request]
  (res/resource-response "index.html"))

(defn content-type-response [response content-type]
  (assoc-in response [:headers "Content-Type"] content-type))

(defn wrap-content-type [handler content-type]
  (fn
    ([request]
     (-> (handler request) (content-type-response content-type)))))

;; Here we are using the threading macro ->
;; check here: https://clojure.org/guides/threading_macros
(def app
  (-> site
      (wrap-resource "public")
      (wrap-content-type "text/html")))

(defn -main
  [& [port]]
  (if port
    (jetty/run-jetty #'app {:port (Integer. port)})
    (println "No port specified, exiting.")))

Restart the app as usual and try it.

Parameters

The most basic way to send values to web applications is using URL-encoded parameters. The Ring provides support for these kinds of parameters via middleware (ring.middleware.params). If passing a single parameters, for example http://localhost:3002/?name=jhon your parameter map will looks like:

{"name" "jhon"}

On another hand, if the parameter name is duplicated, like http://localhost:3001/?name=jhon&name=bob, your parameter map will look like this:

{"name": ["jhon", "bob"]}

Let’s change a bit the core.clj and let’s handle these two scenarios regarding parameters:

(ns boring-clj.core
(:require
   [ring.adapter.jetty :as jetty])
  (:use [ring.middleware.resource]
        [ring.middleware.params]
        [ring.util.response]))

(defn page [name]
  (str "<html><body>"
       (cond
         (vector? name) (clojure.string/join "," name)
         (string? name) (str "String paraam " name)
         :else "no URL params provided"
         )
       "</body></html>"))

(defn handler [{{name "name"} :params}]
  (-> (response (page name))
      (content-type "text/html")))

(def app
  (-> handler wrap-params))

(defn -main
  [& [port]]
  (if port
    (jetty/run-jetty #'app {:port (Integer. port)})
    (println "No port specified, exiting.")))

Here if a single (and unique) parameter is used, we just show it as String param <name>, if multiples, (vector?) we join it as a unique string separated by a comma. Test it with:

Sessions

One another common thing used in web apps are sessions, as expected we need a middleware for it. Let’s update our core.clj, to plain a bit with the session.

(ns boring-clj.core
  (:require
   [ring.adapter.jetty :as jetty])
  (:use [ring.middleware.resource]
        [ring.middleware.params]
        [ring.middleware.session]
        [ring.middleware.reload :refer [wrap-reload]]
        [ring.util.response]))

(defn page [name total-parameters]
  (str "<html><body>"
       "<b>"(str "total parameters " total-parameters) "</b>"
       (cond
         (vector? name) (str "Vector "(clojure.string/join "," name))
         (string? name) (str "String param " name)
         :else "no URL params provided"
         )
       "</body></html>"))

(defn clean-session [session]
  (assoc :session nil))

(defn handler [{session :session {name "name"} :params}]
  (let [count-names (:count-names session 0)
        total-count (count name)
        session (assoc session :count-names (+ total-count count-names))
        ]
    (-> (response (page name (:count-names session)))
        (content-type "text/html")
        (assoc :session session))))

(def app
  (-> handler
      (wrap-params)
      (wrap-reload)
      (wrap-session)))

(defn -main
  [& [port]]
  (if port
    (jetty/run-jetty #'app {:port (Integer. port)})
    (println "No port specified, exiting.")))

Not special here, we update the handler function to extract (using destructuring) the session from the request. Given the functional the approach used by Ring, manage session works a bit differently: here we count all parameters used in each requests replacing the session with an updated session (count-names)

Automatic Reloading

One annoying thing until know is that we need to restart the server on every single change on the code. As mentioned in the beginning, Ring has a library called ring-devel for this job. Include the middleware in your namespace with:

[ring.middleware.reload :refer [wrap-reload]

And update your app handle to:

(def app
  (-> handler
      (wrap-params)
      (wrap-reload)))

Now you just need to run the server once. Once this is very quick and easy to set up, this approach does not reload the application state, so maybe some time is will be required to restart the server. You can check how to do it here.

Conclusions

This post presented the most fundamentals parts of Ring routing Library. We saw the basics concepts about Ring and how the library is organized, passing by the most useful resources we need to build web apps: requests, responses, static resources, parameters, and sessions. By the end, we quickly saw how to automatically reloading the app using a quick approach.

References