Many Javascript developers use nodemon or a backend framework that provides the feature out of the box to reload their web server when code files change to have the latest code running instantly. This makes the development feedback cycle much faster than the alternative of manually restarting the server each time changes are made.
In Clojure, we often re-evaluate our function in the REPL and our server will pick up the changes. There are a few little tricks to remember when doing this and a couple of utilities that we can use to make the workflow faster for ourselves.
Here's an example to demonstrate this.
(ns acme.reload
(:require [ring.middleware.params]
[ring.adapter.jetty :as jetty])
(:gen-class))
(defn app [_request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "response OK"})
(defonce server (atom nil))
(defn start! []
(reset! server
(jetty/run-jetty app
{:port 8080 :join? false})))
(comment
(.stop @server)
(start!))
Now when the server is started with the function start!
we can make an HTTP GET request and get the hardcoded value "response OK".
❯ http :8080
HTTP/1.1 200 OK
Content-Type: text/html
Date: Sun, 03 Sep 2023 12:10:24 GMT
Server: Jetty(9.2.21.v20170120)
Transfer-Encoding: chunked
response OK
Now, if we alter the response string and re-evaluate the app in reply
(defn app [_request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "response still OK"})
;; => #'acme.reload/app
We will still get the same value "response OK" because the response handler is passed to the run-jetty
function as a value.
The first simple step we can do to improve the workflow is to use the reader macro #'
to pass the value as a reference. Doing using this the value is resolved by reference and not as the value the reference presents at evaluation time.
When used it will attempt to return the referenced var. This is useful when you want to talk about the reference/declaration instead of the value it represents.
The only thing we need to change from the code is to add #'
before the app
.
(defn start! []
(reset! server
(jetty/run-jetty #'app
{:port 8080 :join? false})))
By doing this Jetty will pick up the changes whenever we re-evaluate the app function. This is super helpful when working dynamically with the REPL and it is a definite productivity boost.
But if you want to have the same effect when saving source code files you might want to reach out to the wrap-reload
middleware that is part of ring-devel
that can listen to changes on source files and reload the code based on this.
(ns acme.reload
(:require [ring.middleware.params]
[ring.middleware.reload :refer [wrap-reload]]
[ring.adapter.jetty :as jetty])
(:gen-class))
(defn app [_request]
{:status 200
:headers {"Content-Type" "text/html"}
:body "OK"})
(defonce server (atom nil))
(defn start! []
(reset! server
(jetty/run-jetty
(wrap-reload #'app)
{:port 8080 :join? false})))
(comment
(.stop @server)
(start!))
By default, wrap-reload
listens to changes in the src
folder but it takes dirs
as an option if other folders are needed. To summarize, the development workflow can be streamlined by using the var quote #'
to point to vars by reference which makes the latest REPL evaluated code available right away and wrap-reload
middleware to hot reload the server on file changes.
As usual, thanks for reading and I hope you found this useful.