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 thens
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
- Clojure Programming: Blog part 1 by tonsky