Learning Clojure. Part 1

27 Mar, 2018

What is Clojure

Clojure is modern, functional programming language for the JVM. It compiles to Java bytecode, can use any existing Java jar file, instantiate any Java class, and call any Java method. The syntax is elegant and easy to understand.

To learn Clojure, I decided to make a small project — a blog with cooking recipes, yolk.

Consider this post as my notes for building an app from scratch with bare minimum knowledge.

Start

We'll be using leiningen as a build tool. It can also generate a new project skeleton, run the tests, launch REPL, package your project as JAR, and other.

lein new yolk

Keep only project.clj and src/

.
├── project.clj
└── src
    └── yolk
        └── core.clj

Now let's check what clojure version is the latest and update project.clj, where we keep the project dependencies.

; project.clj

(defproject yolk "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.9.0"]])

To download the dependencies, run lein deps. To start REPL, run lein repl.

We'll be using immutant.web for serving web requests.

; project.clj

(defproject yolk "0.1.0-SNAPSHOT"
  :dependencies [
    [org.clojure/clojure "1.9.0"]
    [org.immutant/web    "2.1.9"]
])

Now let's create a simple web server and launch it on localhost:8080.

; src/server.clj

(ns yolk.server
  (:require
    [immutant.web :as web]))

(def app
  (fn [req]
    {:status 200
     :body "hello" }))

(defn -main [& args]
  (web/run app { :port 8080 }))

Here app returns a simple handler — a function taking a request as param and returning a response hash-map.

To pass port as argument of -main, we need to parse args:

(let [args-map (apply array-map args)
      port-str (or (get args-map "--port")
                   "8080")]

Here is a useful snippet to start/stop the server from REPL:

(comment
  (def server (-main "--port" "8080"))
  (web/stop server))

Now we can run the app:

lein run -m yolk.server --port 3000

Routing

Ring library is basically a servlet which provides you with clojure's data types and we can use it for routing, but we better get Compojure.

; project.clj

(defproject yolk "0.1.0-SNAPSHOT"
  :dependencies [
    [org.clojure/clojure "1.9.0"]
    [org.immutant/web    "2.1.9"]
    [compojure           "1.6.0"]
])

Let's define the routes with compojure's macro defroutes:

(compojure/defroutes routes
  (compojure/GET "/" [:as req]
                 {:body "/"})

  (compojure/GET "/post/new" []
                 {:body "new post"})

  (compojure/GET "/post/:post-id" [post-id]
                 {:body post-id})

  (fn [req]
    {:status 404
     :body "404 not found" }))

And now we can update our app to use the new handler:

(def app routes)

Navigating to /, /post/new, /post/yolk, and /rubbish should work now.

Rendering HTML

Let's design the actual post structure.

(def posts
  [{:id    "123-foo",
    :author "sasha",
    :title "Vegan Coconut Lentil Soup"
    :body  "Never has such a flavorful, hearty, and warming meal come together so quickly or using so many pantry staples.",
    :created "2018-03-28"}

   {:id     "456-bar"
    :author "katka"
    :title  "Horchata Latte"
    :body   "Welcome to your dream morning. You wake up early and actually have time to casually sip your latte while reading the paper. But not just any latte. This is an ice-cold horchata latte with dates, cardamom, maple syrup, and fresh cinnamon. Bonus: no milk is used despite the creamy, smooth texture. Here's how to make it happen..."
    :created "2018-03-28"}])

Later we may want to move the content to DB, start using markdown, and be able to upload images, but for now it's just fine.

Okay, how do we render this data on a page? We'll be using rum for that.

Add [rum "0.11.2"] to project.clj and require rum.core in src.

For server-side rendering we can use rum/render-static-markup.

First, we define a component for a post:

(rum/defc post [post]
  [:article
   [:header
     [:h1 (:title post)]
     [:.author (:author post)]]
     [:.created (:created post)]
   [:p (:body post)]])

Then, a component for the page:

(rum/defc page [posts]
  [:html
   [:head
    [:meta {:charset "utf-8"}]
    [:title ""]
    [:style {}]]
   [:body
    [:main
     (for [p posts]
       (post p))]]])

And now let's render the page:

(compojure/GET "/" [:as req]
               {:body (rum/render-static-markup (page posts))})

Stylesheets

Now let's define some basic stylesheets

(def styles
  "body {
    margin: 0;
    padding: 0;
  }
  .page__header {
    background: #000;
    color: #fff;
    text-align: center;
  }
  .page__title {
    margin: 0;
    padding: 1rem;
    font-size: 1rem;
  }
  .page__content {
    padding: 1rem;
  }")

And add it to the page rum component

[:style { :dangerouslySetInnerHTML {:__html styles}}]]

As stylesheets will be growing, let's move them to a separate file resources/style.css and read as a string:

(def styles (slurp (io/resource "style.css")))

Posts

Create posts/ folder on the top level, and move the posts data there. We'll be using edn format for our data.

posts
├── 123-foo
│   └── post.edn
└── 456-bar
    └── post.edn

Reading data should be similiar to what we've done to css.

There are a few differences here, though. Our post is not a resource but a regular file, and we should parse edn to get a clojure data structure. For that we need to require [clojure.edn :as edn].

(def post-ids ["123-foo" "456-bar"])

(for [id post-ids]
  (let [post-path (str "posts/" id "/post.edn")
        post-str  (slurp (io/file post-path))
        p         (edn/read-string post-str)]
    (post p)))

Deployment

I'm not a big expert in docker and it could be implemented in a more sophisticated way.

But what we should do, is building a single standalone executable jar file.

FROM clojure

RUN mkdir -p /usr/src/app/posts
WORKDIR /usr/src/app
COPY project.clj /usr/src/app/
RUN lein deps
COPY . /usr/src/app
RUN lein package

EXPOSE 80:8080
CMD ["java", "-jar", "/usr/src/app/target/yolk.jar"]

For this to work we need to specify a namespace as :main in project.clj:

:main yolk.server

This namespace should also contain

  • a -main function that will get called when the standalone jar is run,
  • a (:gen-class) declaration in the ns form at the top.

Here is my project.clj in the end

(defproject yolk "0.1.0-SNAPSHOT"
  :dependencies [
    [org.clojure/clojure "1.9.0"]
    [ring/ring-core      "1.6.2"]
    [org.immutant/web    "2.1.9"]
    [compojure           "1.6.0"]
    [rum                 "0.11.2"]]

  :main yolk.server

  :profiles {
    :uberjar {
      :aot          [yolk.server]
      :uberjar-name "yolk.jar"
      :auto-clean   false
    }
  }

:aliases { "package" ["do" ["clean"] ["uberjar"]] })

I deployed it to zet's now with a simple command:

now --docker --public

You can see the result here: https://yolk-brnplxfdgk.now.sh/.

Sources: https://zeit.co/sasha/yolk/brnplxfdgk/source?f=src/yolk/server.clj.

References