dependency-injectionclojurecompojurering

Passing state as parameter to a ring handler?


How does one inject state into ring handlers most conveniently (without using global vars)?

Here is an example:

(defroutes main-routes
  (GET "/api/fu" [] (rest-of-the-app the-state)))

(def app
  (-> (handler/api main-routes)))

I would like to get the-state into the compojure handler for main-routes. The state might be something like a map created with:

(defn create-app-state []
  {:db (connect-to-db)
   :log (create-log)})

In a non ring application I would create the state in a main function and start injecting it, or parts of it, as function parameters to the different components of the application.

Can something similar be done with ring's :init function without using a global var?


Solution

  • I've seen this done a couple of ways. The first is using middleware that injects the state as a new key in the request map. For instance:

    (defroutes main-routes
      (GET "/api/fu" [:as request]
        (rest-of-the-app (:app-state request))))
    
    (defn app-middleware [f state]
      (fn [request]
        (f (assoc request :app-state state))))
    
    (def app
      (-> main-routes
          (app-middleware (create-app-state))
          handler/api))
    

    The other approach is to replace the call to defroutes, which behind the scenes will create a handler and assign it to a var, with a function that will accept some state and then create the routes, injecting the state as parameters to function calls within the route definitions:

    (defn app-routes [the-state]
      (compojure.core/routes
        (GET "/api/fu" [] (rest-of-the-app the-state))))
    
    (def app
      (-> (create-app-state)
          app-routes
          api/handler))
    

    Given a choice, I'd probably go with the second approach.