0
votes

I am learning clojure, and I've hit a snag while trying to refactor my web app to make it more functional and less reliant on global state.

The way you make a simple auto-reloading server with ring framework is like this:

(defn handler [req]
  {:status 200
   :body "<h1>Hello World</h1>"
   :headers {}})

(defn -main []
  (jetty/run-jetty (wrap-reload #'handler)
     {:port 12345}))

(source: https://practicalli.github.io/clojure-webapps/middleware-in-ring/wrap-reload.html)

So handler is a global function. It is given as a var reference to wrap-reload. On each request, wrap-reload will reload the entire namespace, and then re-resolve handler reference to a potentially different function. Or at least this is my understanding.

This is all good in this simple example. The trouble starts when I replace this hello world handler with my actual handler, which has all sorts of state bound into it (eg. database connection). This state is initialized once inside -main, and then injected into the routing stack. Something like this:

(defn init-state [args dev]
  (let
   [db (nth args 1 "jdbc:postgresql://localhost/mydb")
    routes (make-routes dev)
    app (make-app-stack routes db)
  {:port (Integer. (nth args 0 3000))
   :route routes
   :dev dev
   :db db
   :app app})

(defn start [state]
  (println "Running in " (if (:dev state) "DEVELOPMENT" "PRODUCTION") " mode")
  (model/create-tables (:db state))
  (jetty/run-jetty (:app state) {:port (:port state)}))

(defn -main [& args]
  (start (init-state args false)))

make-routes generates the router based on compojure library, and make-app-stack then wraps this router into a bunch of middlewares, which will inject global state (like DB string) for the use by handlers.

So how do I add wrap-reload to this setup? #'app macro won't work on local let "variable" (or whatever it's called). Do I need to expose my app as a global variable? Or can I re-generate the entire closure on each request.

My instincts tell me to avoid having global initialization code in the module body and keep all code in the main, but maybe I am overthinking. Should I just type my init-state code as globals and call it a day, or is there a better way?

1

1 Answers

1
votes

I came up with a solution I can live with.

(ns webdev.core
  (:require [ring.middleware.reload :as ring-reload]))

; Defeat private defn
(def reloader #'ring-reload/reloader)

; Other stuff...

(defn load-settings [args dev]
  {:port (Integer. (nth args 0 3000))
   :db (nth args 1 "jdbc:postgresql://localhost/webdev")
   :dev dev})

(defn make-reloading-app [settings]
  (let [reload! (reloader ["src"] true)]
    (fn [request]
      (reload!)
      ((make-app settings) request))))

(defn start [settings]
  (let
    [app
      (if (:dev settings)
        (make-reloading-app settings)
        (make-app settings))]
    (println "Running in " (if (:dev settings) "DEVELOPMENT" "PRODUCTION") " mode")
    (model/create-tables (:db settings))
    (jetty/run-jetty app {:port (:port settings)})))

(defn -main [& args]
  (start (load-settings args false)))

Full code is available here: https://github.com/panta82/clojure-webdev/blob/master/src/webdev/core.clj

Instead of using wrap-reload directly, I use the underlying private reload function. I have to recreate the routing stack in every request, but it seems to work fine in dev. No global state needed :)