15
votes

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.

2

2 Answers

19
votes

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
...
0
votes

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)))))