Skip to content
Daniel Szmulewicz edited this page Nov 30, 2017 · 21 revisions

Raamwerk is the web framework that ships with the system library. Broadly speaking, it can be viewed as a mapping of Ring concepts onto the concept of lifecycle components (Stuart Sierra).

  • Ring routes -> Endpoint component
  • Ring middleware -> Middleware component
  • Ring Handler -> Handler component

Raamwerk is thus comprised of three components: endpoint, middleware and handler. These are standard lifecycle components that operate in concert to allow a declarative specification of a Ring application.

For example, you can declare a Ring application like so:

(defsystem webapp
  [:middleware (new-middleware {:middleware [[wrap-defaults site-defaults]]})
   :endpoints  (component/using (new-endpoint routes) [:middleware])
   :handler (component/using (new-handler) [:endpoints :middleware])
   :server (component/using (new-jetty :port 8080) [:handler])])

Which is equivalent to the pre-componentized form:

(def handler (-> routes
                 (wrap-defaults site-defaults)))
(run-jetty handler {:port 8080})

You may ask yourself, what have we gained so far? Please keep on reading to understand Raamwerk’s value proposition.

Endpoints

First a quick recap, routes are handlers that return a response map or nil. Although we rarely write them like this, this is what they boil down to.

(defn a-route [request]
  (when (= (:uri request) "/home") (response "welcome!")))

An endpoint is a closure over routes with component dependencies in scope.

(defn endpoint [{db :db :as component}]
  (routes ...)

This enables you to access system dependencies in your routes. For example, if you wanted to access a database, the system map for our Ring application would look something like this:

(defsystem webapp
  [:db (new-db options)
   :middleware (new-middleware {:middleware [[wrap-defaults site-defaults]]})
   :endpoints  (component/using (new-endpoint routes) [:db])
   :handler (component/using (new-handler) [:endpoints :middleware])
   :server (component/using (new-jetty :port 8080) [:handler])])

Routing libraries allow us to split routes and recombine them, but they are not concerned with handlers’ requirements in terms of dependencies. Raamwerk aims to fill that gap.

Endpoints can be distributed across libraries, with references to databases, queues or schedulers passed along to handlers as dependencies. This is conducive to a clean architecture with high reuse potential.

For example, a payment library can have routes associated with handlers dispatching work to an in-process queue.

(defn endpoint [{queue :queue :as component}]
  (routes
   (GET "/shop/:id/payment" req (api/payment req queue))
   (GET "/shop/:id/agreement" req (api/agreement req queue))

To integrate those endpoints in any or all of your web apps, you require and declare them in the system map of the web app.

(defsystem webapp
  [:queue (new-queue  :queue-path "/foo")
   :payment-endpoints (component/using (new-endpoint payment/endpoint) [:app-middleware :queue])
   :app-middleware (new-middleware {:middleware [[wrap-defaults site-defaults]]})
   :app-endpoints  (component/using (new-endpoint routes) [:app-middleware])
   :handler (component/using (new-handler) [:app-endpoints :payment-endpoints])
   :server (component/using (new-jetty :port 8080) [:handler])])

Note: Raamwerk doesn’t dictate what routing library to use. It can accommodate any routing library that provides a make-handler function, ie. a function combining routes into one handler. Out of the box, Raamwerk supports compojure and bidi.

Middleware

The middleware component is defined by a map with a key, :middleware, and a vector. Middleware that doesn’t expect arguments (beyond the implicit handler) is specified as is. If it has arguments, it is itself enclosed in a vector where the first element is the middleware and everything that follows are the arguments. For example:

(new-middleware {:middleware [[wrap-not-found (not-found)]
                              [wrap-defaults defaults]
                              wrap-cljsjs
                              wrap-content-type]})

In Ring applications, the order of middleware is of significance. In Raamwerk, middleware functions are applied in the order they appear. The example above is equivalent to the following threaded form:

(def handler (-> routes
                 wrap-content-type
                 wrap-cljsjs
                 (wrap-defaults site-defaults)
                 (wrap-not-found (not-found))))

You can have as many middleware components as your application needs.

Handler

The handler component has logic that will merge all endpoints with their respective middleware. The handler takes the endpoints as dependencies in any order.

Let’s take a look at a more advanced configuration where we want separate routes for our site and API.

(defsystem webapp
  [:middleware (new-middleware {:middleware [[wrap-not-found (not-found)]]})
   :site-middleware (new-middleware {:middleware [[wrap-defaults site-defaults]]})
   :api-middleware (new-middleware {:middleware [[wrap-defaults api-defaults]]})
   :site-endpoints  (component/using (new-endpoint site-routes) [:site-middleware])
   :api-endpoints (component/using (new-endpoint api-routes) [:api-middleware])
   :handler (component/using (new-handler) [:api-endpoints :site-endpoints :middleware])
   :server (component/using (new-jetty :port 8080) [:handler])])

The :middleware component is special. It is the only middleware component that is specified directly in the handler’s dependencies. It is preset to be merged with all endpoints. You can regard it as the root middleware, shared by all endpoints. Depending on the specifics of your Ring application, you either define it to contain a minimal set of middleware shared by all endpoints, or leave it out altogether.

Clone this wiki locally