macchiato-framework / macchiato-core

Ring style HTTP server abstraction for Node.js
MIT License
377 stars 35 forks source link

Idiomatic way to compose routers and have router-specific middleware #12

Closed facundoolano closed 7 years ago

facundoolano commented 7 years ago

First of all, thanks for this project, it really fills the gap for people coming to Clojure from Node.js rather than Java.

I've been playing with the library for some days, based on the lein template and the example projects. All of those have a single file with handlers that also defines a router (which relies on bidi to match routes to handlers) and then "global" middleware that applies to all those routes. I wonder about the idiomatic way to break the project into smaller handler files, smaller routers and possibly router-specific middleware.

For example let's say my project has some JSON API endpoints and a small website for documenting those endpoints. My hunch (i.e. how I would do it with express) would be to have a file with API endpoint handlers and other file for the website handler; each file would have its own router, and ideally I'd like to apply specific middleware to each of the routers (for example: JSON handling and auth for the API but not for the public website).

What I've been doing so far is defining a routes map in each file and merge it into a single router like the one in the examples:

(ns example.routes.api)

(defn api-root [req res raise])

(def api-routes {"/api/v1" {:get api-root}})

(ns example.routes.website
  (:require [example.routes.api :as api])

(defn home [req res raise])
(defn about [req res raise])

(def hmtl-routes {"/" {:get home}
                  "/about" {:get about}})

;; merge the route maps into a bidi friendly structure 
(def routes ["" (merge website-routes api/api-routes)])

;; global router function as defined in the example projects
(defn router [req res raise]
  (if-let [{:keys [handler route-params]} (bidi/match-route* routes (:uri req) req)]
    (handler (assoc req :route-params route-params) res raise)
    (not-found req res raise)))

The thing with this approach is it still uses a single router so I can't easily apply middleware to api-routes that doesn't affect website-routes, so in practice I go to every api handler and wrap it in the api specific middleware. I'd rather have separate routers with their own middleware and some way to compose them together when defining the app.

I realize that I can come up with a smarter, composable version of the router function, but I wanted to hear someone else's thoughts before going too far in that direction, in case there's already a Ring/Clojure way of solving this kind of situation (I don't have any Clojure web dev background, and most of what I've found googling refers to Compojure, which doesn't seem to be relevant here).

Thanks!

yogthos commented 7 years ago

I find that usually you end up with a common middleware stack similar to the defaults, and then you apply route specific middleware on top of it. Stuff like sessions, cookies, request params, and so on tend to be useful for majority of the routes.

I think that the approach of wrapping individual handlers that need custom functionality with specific middleware is fine. You could also write a macro to handle the boilerplate, e.g:

(defmacro json-hanlder [fn-name args & body]
  `(def ~fn-name
     (wrap-json
       (fn ~args ~@body))))

(json-handler api-root [req res raise] ...)

(def api-routes {"/api/v1" {:get api-root}})

One way to wrap middleware for a set of routes would be to compose router calls. You could modify the router to accept the routes to match when it's created:

(defn make-router [routes]
  (fn [req res raise]
    (if-let [{:keys [handler route-params]} (bidi/match-route* routes (:uri req) req)]
      (handler (assoc req :route-params route-params) res raise)
      (not-found req res raise))))

(def router (make-router routes))

I also recommend taking a look at the router library, that supports composing routes in its API. There's an example using the library here.

facundoolano commented 7 years ago

Great, thanks for the pointers!