
How I wrote a web application using only clojure

Recently I met an interesting language - clojure. I immediately liked the lazy and immutable collections, stm, macros, lots of brackets and dsl for all occasions.
And I decided to try to make a web application using only clojure.
application
It was intended to create a simple subtitle seeker that:
- every 5 minutes indexes new subtitles on addicted, notabenoid and other services;
- has a one-page web-interface with search without page reloading;
- shows in the web-interface the number of indexed subtitles and changes it when new ones appear;
- has a simple api for interacting with the desktop client .
Parsers
Surprisingly, parsers were simple and convenient to write. At first, it seemed that there were a lot of brackets, but threading macros ( -> , - >> , ->, and - <>> - passing the result of the argument to the next expression) helped a lot.
For example, a piece of the notabenoid parser doing the same thing in python and clojure:
clojure | python |
|
|
16 brackets | 14 brackets |
Server side
Server
As the server, I chose the http-kit , mainly because I wanted web sockets. And it’s very simple to use them here, for example, sending all the clients the number of indexed subtitles after the update will look like this:
(add-watch total-count :notifications
#(doseq [con @subscribers]
(send! con (prn-str {:total-count %4}))))
Routing
For routing - compojure . There are no differences from django and other popular frameworks:
(defroutes main-routes
(GET "/" [] (views/index-page))
(GET "/api/list-languages/" {params :params} (api/list-languages params))
(GET "/notifications/" [] push/notifications)
(route/resources const/static-path))
API
Since we use clojure everywhere, our api should return the result in the native data structures and in json (for the desktop client in python). I couldn’t find the library that could (I already found) , so I had to cycle a bit and invent my own mini dsl:
(defn- get-writer
"Get writer from params"
[params]
(if (= (:format params) "json")
json/write-str
prn-str))
(defmacro defapi
"Define api method"
[name doc args & body]
`(defn ~name ~args
((get-writer (first ~args))
~@body)))
And as a simple use case:
(defapi list-languages
"List all available languages"
[params]
(models/list-languages))
View
For html rendering, I used a special dsl - hiccup , the template with it looks a bit "Martian":
(defn index-page []
(html5 [:head
[:title "Subman - subtitle search service"]
[:body
[:h1 "Welcome to subman!"]]))
Styles
For styles, clojure also has its own dsl - garden . The code with it also looks strange:
(defstyles main
[:.search-input {:z-index 100
:background-color "#fff"}]
[:.info-box {:text-align "center"
:font-size (px 18)}]
[:.search-result-holder {:padding-left 0
:padding-right 0}])
Client part
I wrote the client part not entirely in clojure, but in clojurescript, which eventually compiles into javascript. As a framework, I used reagent - binding for react.js for clojure, not checking objects for changes every second (thanks to atoms) and using hiccup-like dsl to describe components:
(defn info-box
"Show info box"
[text]
[:div.container.col-xs-12.info-box
[:h2 text]])
Everything is very good here, until you need to work directly with js libraries. For example, the code for connecting typeahead to the search field:
(defn init-autocomplete
"Initiale autocomplete"
[query langs sources]
(let [input ($ "#search-input")]
(.typeahead input
(js-obj "highlight" true)
(js-obj "source"
(fn [query cb]
(cb (apply array
(take const/autocomplete-limit
(map #(js-obj "value" %)
(get-completion query
@langs
@sources))))))))
(.on input "typeahead:closed" (fn []
(reset! query (.val input))))))
UPD: After a little refactoring, the code became less scary:
(defn completion-source
"Source for typeahead autocompletion"
[langs sources query cb]
(cb (->> (get-completion query
@langs
@sources)
(map #(js-obj "value" %))
(take const/autocomplete-limit)
(apply array))))
(defn init-autocomplete
"Initiale autocomplete"
[query langs sources]
(let [input ($ "#search-input")]
(.typeahead input
#js {:highlight true}
#js {:source (partial completion-source
langs sources)})
(.on input "typeahead:closed"
#(reset! query (.val input)))))
And even the size of the “compiled” file was not so big - only 290kb.
As a huge plus, using clojure along with clojurescript - you can write one code for the client and server using cljx .
conclusions
Although clojure allows you to develop web applications without the knowledge and use of html, css and javascript, I would not dare to do production projects like this.
The source code of the result.
The result itself.