DEV Community

Prabu
Prabu

Posted on

Watch and auto reload a Clojure pedestal service on source change

The Problem

I started playing with pedestal to create Clojure micro services. Coming from the Nodejs world, and Clojure being a REPL driven development language, I naturally expected to have watch for code changes and reload without having to restart the server. I searched around and found solutions, but I couldn't get it to work trivially. This guide is meant to serve as a step-by-step guide to achieve this

Getting Started

Create a new pedestal service project

lein new pedestal-service my-service

Now, run the server

lein run

Go to http://localhost:8080 (default port) and you will see "Hello World!"

REPL-driven Development

Now that you have successfully seen the service working, lets jump into REPL-driven development

Kill the running server

and then start a nREPL using

lein repl

Once the REPL starts, connect your IDE (I used Calva on Visual Studio Code) to the REPL (refer here for a detailed Calva setup guide)

Once the REPL starts, you can call the run-dev function to start the server in dev mode

(def dev-server (run-dev))
Enter fullscreen mode Exit fullscreen mode

Now head on to http://localhost:8080 to see the browser print "Hello World!" again.

Now open service.clj and try to modify the handler function for the '/' route home-page to return a different message

(defn home-page
  [request]
  (ring-resp/response "Modified message!"))
Enter fullscreen mode Exit fullscreen mode

Try to reload the browser and you will still see "Hello World!". Lets fix this!

Enter ns-tracker

This is the initial service function in service.clj

;; Tabular routes
(def routes #{["/" :get (conj common-interceptors `home-page)]
              ["/about" :get (conj common-interceptors `about-page)]})

(def service {:env :prod
              ::http/routes routes
              ::http/resource-path "/public"
              ::http/type :jetty
              ::http/port 8080
              ::http/container-options {:h2c? true
                                        :h2? false
                                        :ssl? false}})
Enter fullscreen mode Exit fullscreen mode

Lets modify the routes var to a function that returns the same route

(defn routes []
  #{["/" :get (conj common-interceptors `home-page)]
    ["/about" :get (conj common-interceptors `about-page)]})
Enter fullscreen mode Exit fullscreen mode

Now, open server.clj where the run-dev function is defined. This is the correct place to track for source changes and reload.

ns-tracker library provides a function that tracks one or more source directories for file changes that can be used to reload the modified namespaces on the fly. However, this does not work standalone. You need to wrap the routes map of the service around a function and associate the function to the ::http/routes entry in the service map

Require ns-tracker in server.clj

(:require [ns-tracker.core :refer [ns-tracker]])
Enter fullscreen mode Exit fullscreen mode

Define the source directories to be tracked by ns-tracker

(defonce modified-namespaces
  (ns-tracker ["src" "test"]))
Enter fullscreen mode Exit fullscreen mode

Define a watch-routes-fn

(defn watch-routes-fn [routes]
  (fn []
    (doseq [ns-sym (modified-namespaces)]
      (require ns-sym :reload))
    (route/expand-routes routes)))
Enter fullscreen mode Exit fullscreen mode

The watch-routes-fn returns a function that closes in the routes and calls route/expand-routes on it when invoked

Finally, we wrap the ::server/routes entry in the service config map with the watch-routes-fn function call like

(defn run-dev
  "The entry-point for 'lein run-dev'"
  [& args]
  (println "\nCreating your [DEV] server...")
  (-> service/service ;; start with production configuration
      (merge {:env :dev
              ;; do not block thread that starts web server
              ::server/join? false
              ;; Routes can be a function that resolve routes,
              ;;  we can use this to set the routes to be reloadable
              ::server/routes (watch-routes-fn (service/routes))
              ;; all origins are allowed in dev mode
              ::server/allowed-origins {:creds true :allowed-origins (constantly true)}
              ;; Content Security Policy (CSP) is mostly turned off in dev mode
              ::server/secure-headers {:content-security-policy-settings {:object-src "'none'"}}})
      ;; Wire up interceptor chains
      server/default-interceptors
      server/dev-interceptors
      server/create-server
      server/start))
Enter fullscreen mode Exit fullscreen mode

See it working!

Restart the nREPL and connect your IDE to it. Now, call

(def dev-server (run-dev))
Enter fullscreen mode Exit fullscreen mode

Modify the handler to return a different string and head on to the browser to test it. You should now see the modified message show up!

Top comments (0)