How to create a simple clojure project from scratch and create a HTTP service, with routing and SSR.
In an empty dir, create a deps.edn
with this content:
{:paths ["src" "resources"] :deps {org.clojure/clojure {:mvn/version "1.10.1"} io.pedestal/pedestal.service {:mvn/version "0.5.8"} io.pedestal/pedestal.jetty {:mvn/version "0.5.8"} hiccup/hiccup {:mvn/version "2.0.0-alpha2"}}}
Let's understand these names:
:paths
: a list of dirs that will be availble on the JVM classpath. Learn more about it:deps
: A map of dependencies and it's versionsorg.clojure/clojure
: the clojure that we will useio.pedestal/pedestal.service
: the pedestal core libraryio.pedestal/pedestal.jetty
: the adaptor that expose the pedestal core into a jetty http
interface (docs)
hiccup/hiccup
: an cool way to generate HTML in clojure
(docs)
At this moment, we can start the REPL. Just type clj
or use your editor to start one.
Then create recursively the dirs src/clj_ssr_app
, then the
file src/clj_ssr_app/main.clj
Let's write this main.clj
(ns clj-ssr-app.main) ;; << this should matchclj_ssr_app/main.clj
insidesrc
orresources
(defn dev-start [& _] (prn :hello-world))
Now let's test it on REPL
(require 'clj-ssr-app.main :reload) nil (clj-ssr-app.main/dev-start) :hello-world nil
Ok, everything working.
Here I will explain in code comments
(ns clj-ssr-app.main (:require [io.pedestal.http :as http])) ;; our first handler (defn hello [req] {:body "Hello world!" :status 200}) ;; our route table ;; each route is an array with the fileds ;; - the path ;; - the method ;; - the handler function ;; - the keyword :route-name (we will se more about this in a moment) ;; - a unique name for this route (def routes #{["/" :get hello :route-name :hello]}) (defonce http-state (atom nil)) (defn dev-start [& _] (swap! http-state (fn [st] ;; if there is something running, stop it (some-> st http/stop) (-> {::http/routes routes ::http/port 8080 ::http/join? false ::http/type :jetty} http/default-interceptors http/dev-interceptors http/create-server http/start))))
Now back on our repl
;; reload your changes (require 'clj-ssr-app.main :reload) nil ;; call start again (clj-ssr-app.main/dev-start)
Now, if you connect your browser on localhost:8080 your should see a "hello world" message.
Let's work on make hello
function into a HTML response
(ns clj-ssr-app.main (:require [io.pedestal.http :as http] ;; add hiccup [hiccup2.core :as h])) (defn hello [req] {:body (->> [:html [:head [:title "Hello world!"]] [:body [:p {:style {:background-color "lightgeen"}} "Hello from HTML"]]] ;; hiccup2 by default generates xhtml. (h/html {:mode :html}) ;; we need to manually prepend this to make the document valid (str "\n")) ;; we need this headers to talk to the browser that it's a HTML response :headers {"Content-Type" "text/html"} :status 200}) ;; rest of the file remains with no change
Ok, now back on REPL
;; reload your changes (require 'clj-ssr-app.main :reload) nil ;; restart the HTTP server (we will have hot-reload in a near future) (clj-ssr-app.main/dev-start)
now if you refresh your browser, you will see the message in HTML :)
Pedestal has the concept of "interceptor", that is similar to "middlewares": help you to transform requests/responses.
Let's create a interceptor that handles HTML
(def html-response
"If the response contains a key :html
,
it take the value of these key,
turns into HTML via hiccup,
assoc this HTML in the body
and set the Content-Type of the response to text/html"
{:name ::html-response
:leave (fn [{:keys [response]
:as ctx}]
(if (contains? response :html)
(let [html-body (->> response
:html
(h/html {:mode :html})
(str "\n"))]
(assoc ctx :response (-> response
(assoc :body html-body)
(assoc-in [:headers "Content-Type"] "text/html"))))
ctx))})
Now we can add this interceptor to our route
;; the 3nth element can be: ;; A- A function (let's call it handler) that receive a request and return a response ;; B- An interceptor ;; C- An array of interceptors, that may end on a handler function (def routes #{["/" :get [html-response hello] :route-name :hello]})
At this moment, if you reload/restart the http serve, everything should continue to work
But now we can simplify our handler
(defn hello [req] {:html [:html [:head [:title "Hello world!" [:body [:p {:style {:background-color "lightgeen"}} "Hello from HTML"]]]]] :status 200})
After call again require/reload/dev-main
stuff, you should see no difference on the browser
Let's add a new route and links between them
This section will be done in code comments
;; add [io.pedestal.http.route :as route] to your namespace (defn hello [req] {:html [:html [:head [:title "Hello world!" [:body [:p {:style {:background-color "lightgeen"}} "Hello from HTML in green"] ;; let's add a link to yellow page here [:a {:href (route/url-for :hello-yellow)} "go to yellow"]]]]] :status 200}) ;; Create this new handler for :hello-yellow page (defn hello-yellow [req] {:html [:html [:head [:title "Hello world!" [:body [:p {:style {:background-color "lightyellow"}} "Hello from HTML in yellow"] [:a {:href (route/url-for :hello)} "go to green"]]]]] :status 200}) (def routes #{["/" :get [html-response hello] :route-name :hello] ;; and add to your routes ["/yellow" :get [html-response hello-yellow] :route-name :hello-yellow]})