15

I have a ring+compojure application and I want to apply different middleware depending on whether the route is part of the web application or part of the api (which is json based).

I found some answers to this question on stack overflow and other forums, but these answers seem more complicated than the solution I've been using. I wanted to know if there are drawbacks with how I'm doing it and what I may be missing in my solution. A very simplified version of what I'm doing is

  (defroutes app-routes
    (GET "/" [req] dump-req)
    (route/not-found "Not Found"))

(defroutes api-routes
  (GET "/api" [req] dump-req))

(def app
  (routes (-> api-routes
              (wrap-defaults api-defaults))
          (-> app-routes
              (wrap-defaults site-defaults))))

Note that there is more middleware than I have shown here.

The only 'restriction' I've encountered is that as the app-routes has the not-found route, it needs to come last or it will be triggered before finding the api routes.

This seems simpler and more flexible than some of the other solutions I've found, which appear to either use additional conditional middleware, such as ring.middleware.conditional or what seems to me as more complex routing definitions where there is an additional defroutes layer and the need to define defroutes with ANY "*" etc.

I suspect there is something subtle I'm missing here and while my approach seems to work, it will cause unexpected behaviour or results in some situations etc.

Tim X
  • 4,158
  • 1
  • 20
  • 26

2 Answers2

20

You are correct, the ordering matters and there is a subtlety you are missing - the middleware you apply to api-routes is executed for all requests.

Consider this code:

(defn wrap-app-middleware
  [handler]
  (fn [req]
    (println "App Middleware")
    (handler req)))

(defn wrap-api-middleware
  [handler]
  (fn [req]
    (println "API Middleware")
    (handler req)))

(defroutes app-routes
  (GET "/" _ "App")
  (route/not-found "Not Found"))

(defroutes api-routes
  (GET "/api" _ "API"))

(def app
  (routes (-> api-routes
              (wrap-api-middleware))
          (-> app-routes
              (wrap-app-middleware))))

and repl session:

> (require '[ring.mock.request :as mock])
> (app (mock/request :get "/api"))
API Middleware
...
> (app (mock/request :get "/"))
API Middleware
App Middleware
...

Compojure has a nice feature and helper that applies middleware to routes after they have been matched - wrap-routes

(def app
  (routes (-> api-routes
              (wrap-routes wrap-api-middleware))
          (-> app-routes
              (wrap-routes wrap-app-middleware))
          (route/not-found "Not Found")))

> (app (mock/request :get "/api"))
API Middleware
...
> (app (mock/request :get "/"))
App Middleware
...
Kyle
  • 21,978
  • 2
  • 60
  • 61
  • Thanks, you hit my concern on the head and more importantly, pointed me in the right direction with the wrap-routes pointer. – Tim X Jan 19 '15 at 04:33
  • 1
    Just flag a warning with this approach. This does not work correctly with the ring wrap-defaults middleware using the site-defaults config. The problem is that it breaks the route/not-found handler. Seems to be due to wrap-defaults inserting response headers into a nil response, thereby making it appear that a handler has processed the request. Results in a return code of 200 and no content. Have logged an issue with ring-defaults. – Tim X Jan 21 '15 at 02:45
  • 1
    If you have the option you can just create two ring apps and mount them in different web contexts. Using immutant you can simply create two handlers, use different middleware for both and then mount them in different paths, e.g. `(web/run app)` and `(web/run api :path "/api")` – egli Feb 18 '15 at 17:13
0

A simpler solution may be... (I'm using this in my application, I adapted the code to your example)

(defn make-api-handler
  []
  (-> api-routes
      (wrap-defaults api-defaults)))

(defn make-app-handler
  []
  (-> app-routes
      (wrap-defaults site-defaults)))

(def app
  (let [api-handler-fn (make-api-handler)
        app-handler-fn (make-app-handler)]
    (fn [request]
      (if (clojure.string/starts-with? (:uri request) "/api")
        (api-handler-fn request)
        (app-handler-fn request)))))
Xavi
  • 583
  • 1
  • 7
  • 13