Simple Web with Clojure: Http-Kit, Compojure, Hiccup

There lot of Lisp-like programming language which claimed by many of people offering peace of mind in programming and powerful language to expressing idea. Yukari end up with Clojure which fond for while quite suffering when first touching (Yukari have touch Racket and Common Lisp yet still not get used).

The reason Yukari fond with Clojure is the language bit less lisp-ty and easy to grasp. Clojure aiming to reduce parentesses which nice and also keeping the benefit of Lisp intact, let say Lisp with contemporary touch.

Ultimately, Yukari pursuing thinking experience in another programming language since each language have it own culture, perspective and wisdom which enhance Yukari mind. Despite that same tool to write program, it's making you think differently, is'nt?

Meanwhile, Lisp itself quite niche in term of popularity, let alone Clojure. But Clojure have a lot of resource to learn by community and ecosystem support from Java (Multiplatform uhum 3 Billion devices run java).

(defn fib [acc x]
    (if (x = 0) acc (fib (+ acc x) (- x 1))))

(defn -main []
    (print UwU))

This what Clojure look like, seems programming language used by Cthulhu (pun intended). Enough the introduction, moving on.

How create simple web in clojure ?

To follow this step, Yukari assume you already installing Clojure, Leiningen (want a challange? then use Clojure tools.build for building project) and code editor that already installed with Clojure plugin to enhance developer experience (Notepad++, VSCode, Vim, Emacs: Yukari won't judge).

Setting up the project

First create a project using :

$ lein new app <your-app-name>

Attention: do not write the dollar sign, it just indicator that it was shell command.

In this example, Yukari will use moe. Leiningen will create folder with those name, and get inside it.

Let check if the project is works fine, by running it:

$ lein run

After generation of project folder finish, find project.clj files inside it. Then in dependencies field assure you add our library that will used for making web:

  :dependencies [
    [org.clojure/clojure "1.12.2"]
    ; add this below
    [http-kit "2.8.1"] 
    [compojure "1.7.2"]
    [hiccup "2.0.0"]]

Save the file and run :

$ lein deps

To fetching thet dependecies we need from remote registry.

Now, we done setup the project. Time to code! go to src/<your-app-name>/ there will a file name core.clj which our program entry point. The content look like this:

(ns moe.core ; (1)
  (:gen-class))

(defn -main ; (2)
  "I don't do a whole lot ... yet." ;(4)
  [& args]
  (println "Hello, World!")) ;(3)

The explanation:

  1. Is definition of our program namespace, in this case moe.core, in this place we will use to import our dependencies.
  2. -main is our entrypoint function, the program will start by invoking this function.
  3. To print Hello, World with newline at endline.
  4. This work as documentation for our function, skip this for now.

Invoking web server

In this phase, we will learn to use Http-kit as our web server. First let we :use it:

(ns moe.core
  (:gen-class)
  (:use [org.httpkit.server :as s]) ; (1)
  )
  1. We import the Http-Kit server with alias s, this alias help us to invoking the function. We could use :refer like (:use [org.httpkit.server :refer [run-server]]), which simple but risky to name collision. For example we go to alias.

Then we create function to handle basic request for our web server.

(defn app 
    [req] ; (1)
    {
        :status 200 
        :headers {"Content-Type" "text/html"}
        :body "UwU"
    } ; (2)
)
  1. is parameter of app function to read incoming request detail.
  2. a record that contain label that will represent our http response for the request, the example above said we send 200 OK with Content-Type HTTP headers with value text/html and body response UwU (which we should put our HTML document here).

Now, we bind the function with our web server function (change entrypoint like this):

(defn -main
  "our web server"
  [& args]
    (s/run-server app {:port 8080})) ;(1)
  1. s/run-server will start web server with app function to handle in HTTP comming connection at port 8080.

Save then run the project, and open in browser http://localhost:8080.

Routing web request

Now, we had the web server up and running. Then we try to mapping specific function to handle certain URL request, known as routing. Let :use compojure:

(ns moe.core
  (:gen-class)
  (:use 
    [org.httpkit.server :as s]
    ; (1) add this below
    [compojure.core :refer [defroutes, GET, context, ANY]]
    ))
  1. Is telling us to import specified function that provided by compojure.core without alias by using :refer attribute. Case above we could use defroutes, GET, context, ANY functions without adding compojure.core/defroutes like s/run-server.

Lets delete the app function and redefine it for our web server route:

(defroutes app
    (GET "/ping" [] ping) ; (1)
    (GET "/uwu/:name" [name] uwu-ing); (2)
) 
  1. We define route that accept GET request on /ping url, then it will invoke ping function (will define it latter on), [] it to catch parameter inside route.
  2. This example using parameter/slug that will extracted from URL, in this case we define :name paramater that will catch /uwu/yukari as {:params {:name "yukari"}}, [name] define what pattern we will catch and deliver it to uwu-ing function.

Next, we define function to handle certain request (above the app defroutes).

(defn ping [req]
    {
        :status 200
        :headers {"Content-Type" "text/html"}
        :body "pong"
    }
)

(defn uwu-ing [req]
    (let [name (:name (:params request)))]
    {
        :status 200
        :headers {"Content-Type" "text/html"}
        :body (str "UwU to you, " (or name "visitor"))
    }
)

Save and run the projects.

Serving web HTML

We at the last piece of cake. To build HTML looking view, we need to serve it as string at :body when we response the request. Let :use the library:

(ns moe.core
  (:gen-class)
  (:use 
    [org.httpkit.server :as s]
    [compojure.core :refer [defroutes, GET, context, ANY]]
    ; (1) add this below
    [hiccup.page :refer [html5]]
    ))
  1. To import hiccup function, in this case we only need html5 function.

Hiccup is library to generate HTML from Clojure pair attribute, like this.

(html5 [:section [:p "Yukari~"] [:p "code in Clojure"]])

into

<section>
    <p>Yukari~</p>
    <p>code in Clojure</p>
</section>

Pretty short and clean to read. Of course, this would exhausting to convert existing HTML into Hiccup format, so use html2hiccup.dev.

So let convert the uwu-ing function to use hiccup.

(defn uwu-ing [req]
    (let [
        name (:name (:params request)))
        label (str "UwU to you, " (or name "visitor"))
        ]
    {
        :status 200
        :headers {"Content-Type" "text/html"}
        :body (html5 
            [:html 
                [:head 
                    [:title label]]
                [:body label]])})

Save and run the projects.

Conclusion

Clojure could pretty confusing at first due it eccentric way of doing thing (especially program not have introduced to lisp before), but code in Clojure easy to read and to understand.

For me, who prefer static-typing like Dart, Clojure maybe the another dynamic typing that I found interesting and got a bit grasp of why many people implement Clojure-like language such Janet and Jank.

Although, this still small portion of Clojure. There still more for Yukari to learn (and hopefuly share to Yukari's dear reader).

Reference: