How does one inject state into ring handlers most conveniently (without using global vars)?
Here is an example:
(defroutes main-routes
(GET \"/api/
In addition to what Alex described some routing frameworks for ring
have a place for additional arguments which can be accessed by all handlers. In reitit
this would work by putting custom objects under :data
:
(reiti.ring/ring-handler
(reiti.ring/router
[ ["/api"
["/math" {:get {:parameters {:query {:x int?, :y int?}}
:responses {200 {:body {:total pos-int?}}}
:handler (fn [{{{:keys [x y]} :query} :parameters}]
{:status 200
:body {:total (+ x y)}})}}]] ]
{:syntax :bracket
:exception pretty/exception
:data {:your-custom-data your-custom-data
:coercion reitit.coercion.spec/coercion
:muuntaja m/instance
:middleware []}}))
In your handler you're supposed to only work with :parameters
, but you will be able to access your custom data by selecting :reitit.core/match
and :data
. The argument that the handler receives is based entirely on this:
(defrecord Match [template data result path-params path])
(defrecord PartialMatch [template data result path-params required])
The "correct" way to do this is to use a dynamically bound var. You define a var with:
(def ^:dynamic some-state nil)
And then you create some ring middleware which binds the var for each handler call:
(defn wrap-some-state-middleware [handler some-state-value]
(fn [request]
(bind [some-state some-state-value]
(handler request))))
You would use this to inject dependencies by using this in your 'main' function where you launch the server:
(def app (-> handler
(wrap-some-state-middleware {:db ... :log ...})))
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.