Skip to content

Clojure in the server

Subhash Gopalakrishnan edited this page Feb 16, 2015 · 13 revisions

Now that we have things working within the browser, let's hit the highway and set up the project to work from a server, shall we? First, for some reorg:

Code reorganization

  • First, let's make space for the Clojure code that we will soon be creating. Create new folders src/clj and src/cljs and move the existing clj_stack under src/cljs
  • Create a new folder for holding static files and resources - resources/public
  • Modify project.clj to update obsolete paths and include dependencies. Pedestal is a web application framework that helps in routing and serving HTTP requests while the underlying Ring framework takes care of low-level HTTP.
(defproject clj-stack "0.1.0-SNAPSHOT"
  :description "FIXME: write this!"
  :url "http://example.com/FIXME"

  :dependencies [[org.clojure/clojure "1.6.0"]
                 [org.clojure/clojurescript "0.0-2755"]
                 [org.clojure/core.async "0.1.346.0-17112a-alpha"]
                 [org.omcljs/om "0.8.8"]
                 [io.pedestal/pedestal.service "0.3.1"]
                 [io.pedestal/pedestal.jetty "0.3.1"]
                 [ch.qos.logback/logback-classic "1.1.2" :exclusions [org.slf4j/slf4j-api]]
                 [org.slf4j/jul-to-slf4j "1.7.7"]
                 [org.slf4j/jcl-over-slf4j "1.7.7"]
                 [org.slf4j/log4j-over-slf4j "1.7.7"]
                 [org.clojure/tools.namespace "0.2.9"]]

  :plugins [[lein-cljsbuild "1.0.4"]]

  :source-paths ["src/clj" "src/cljs" "target/classes"]

  :clean-targets ["resources/public/clj_stack" "resources/public/clj_stack.js"]
  
  :main ^{:skip-aot true} clj_stack.server

  :cljsbuild {
    :builds [{:id "clj-stack"
              :source-paths ["src/cljs"]
              :compiler {
                :output-to "resources/public/clj_stack.js"
                :output-dir "resources/public"
                :optimizations :none
                :cache-analysis true
                :source-map true}}]})

Server and service

The server is the running instance of our application. src\clj\clj_stack\server.clj:

(ns clj_stack.server
  (:gen-class) ; for -main method in uberjar
  (:require [io.pedestal.http :as server]
            [clj_stack.service :as service]
            [clojure.tools.namespace.repl :refer [refresh]]))

;; This is an adapted service map, that can be started and stopped
;; From the REPL you can call server/start and server/stop on this service
(defonce runnable-service (server/create-server service/service))

The service handles all incoming requests. src\clj\clj_stack\service.clj:

(ns clj_stack.service
  (:require [io.pedestal.http :as bootstrap]
            [io.pedestal.http.route :as route]
            [io.pedestal.http.body-params :as body-params]
            [io.pedestal.http.route.definition :refer [defroutes]]
            [ring.util.response :as ring-resp]))


(defn home-page
  [request]
  (ring-resp/response "Hello World"))


(defroutes routes
  [[["/" {:get home-page}]]])

(def service {:env :prod
              ::bootstrap/routes routes
              ::bootstrap/resource-path "/public"
              ::bootstrap/type :jetty
              ::bootstrap/port 8080})

From the project dir, execute lein repl. This will start the repl in the clj_stack.server namespace (Note the :main clj_stack.server configuration in project.clj). Now, you can run (server/start-server runnable-service) to start up the service. Point the browser to localhost:8080 and you will see a greeting again, only this time it comes over HTTP.

Rendering HTML files

Before we can render our index.html from the server, we need to alter some of the paths of the javascript dependencies so that they are fetched from the target of the cljs compilation (resources/public):

        <script src="goog/base.js" type="text/javascript"></script>
        <script src="clj_stack.js" type="text/javascript"></script>

Alter the clj_stack.service/home-page function to:

(defn home-page
  [request]
  (-> (ring-resp/file-response "index.html")
      (ring-resp/content-type "text/html")))

This uses the ring API to render the HTML file as the response and set the Content-Type of the response to text/html.

But, for this to work, we need to reload the server. This is a 3-step process or stopping the server, reloading the namespace and starting the server again. Hit Ctrl+C in the server repl to get the prompt and enter the following:

(server/stop runnable-service)
(refresh)
(server/start runnable-service)

Once the servers starts up again, open the browser at localhost:8080 and you will see our Fake YouTube app work in all its glory

Data API

Since we have a fully working server, it seems to be a shame to maintain the video list locally in the client. Especially since a browser refresh will undo all the hard work of adding new videos! In order to move the data model to the server-side, we need to add the following: an atom to track the model and a web-service function to return the videos in edn format (EDN is considered an improvement from JSON and is handy here because it's a subset of the Clojure notation and therefore does not require any marshalling/unmarshalling logic)

(def data-model 
  (atom 
   {:videos
    [{:id 1 :title "Intro to Datomic" :url "http://www.youtube.com/embed/RKcqYZZ9RDY"}
    {:id 2 :title "The Functional Final Frontier" :url "http://www.youtube.com/embed/DMtwq3QtddY"}]}))

(defn videos [request]
  (bootstrap/edn-response @data-model))

We also need to add a route that will respond to /videos:

(defroutes routes
  [[["/" {:get home-page}]
    ["/videos" {:get videos}]]])

Once you restart the server, try running this: curl http://localhost:8080/videos and you will see the expected edn response from the server.

In order to update the client to use this data API, we will be using a library called om-sync which will come in handy later when we need to automate the communication between the client and the server. For now, add these dependencies:

project.clj - [om-sync "0.1.1"] (under :dependencies)

core.cljs - [om-sync.util :refer [edn-xhr]] (as a :require clause)

To finish, replace the (om/root ..) call at the end to the following instead:

(edn-xhr 
 (let [target (. js/document (getElementById "app"))]
   {:method :get 
    :url "/videos"
    :on-complete #(om/root app-view % {:target target})}))

edn-xhr makes an asynchronous request to the said URL and on receiving a response calls the callback function which roots the view to the application model received

By emptying the client-side application model and refreshing the browser, you can verify that the data is indeed initialized from the server:

; core.cljs
(def app-model (atom {:videos []}))

Model updates

Since the data-model is now maintained in the server, we need to update the server for every change that happens in the client-side application model.

Let's start by adding a couple of URL endpoints and corresponding handlers for adding and deleting videos

; service.clj

(defn delete-video [request]
  (let [id (Integer/parseInt (get-in request [:path-params :id]))]
    (swap! data-model
      (fn [d]
        (update-in d [:videos]
          #(for [el % :when (not= id (:id el))] el))))
    (bootstrap/edn-response {:id id :data data-model})))

(defn add-video [request]
  (let [video (:edn-params (body-params/edn-parser request))]
    (swap! data-model (fn [d] (update-in d [:videos] #(conj % video))))
    (ring-resp/response "ok")))

(defroutes routes
  [[["/" {:get home-page}]
    ["/videos" {:get videos :post add-video}]
    ["/videos/:id" {:delete delete-video}]]])

The client requires corresponding changes while responding to the add and remove actions, whereby a edn-xhr asynchronous call relays the change in the application model to the server

; core.cljs

(defn add-video [videos owner]
  (let [title (-> (om/get-node owner "new-video-title") .-value)
        url (-> (om/get-node owner "new-video-url") .-value)
        video-data {:id (rand-int 1000) :title title :url url}]
    (om/transact! videos #(conj % video-data))
    (println "app-model: " @app-model)
    (edn-xhr {:url "/videos" :method :post :data video-data})
    (om/set-state! owner :new-video-name "")
    (om/set-state! owner :new-video-url "")))

(defn delete-video [videos del-video]
  (om/transact! videos
    (fn [vs] (vec (remove #(= % del-video) vs))))
  (edn-xhr {:url (str "/videos/" (:id del-video)) :method :delete}))

Make sure you play with the application to convince yourself that the data model changes in accordance with the user action. Any server restart should reset the model state to its initial value. Now, all that remains is for us to persist this data model and attempt some analysis on it. And that brings us to Clojure in the Database