Defining routes

Contents

  1. Routes
  2. Return values
  3. Static Files
  4. Organizing application routes
  5. Restricting access
  6. Specifying Access Rules

Luminus uses Compojure to define application routes. The routes are the entry points to your application and are used to establish a communiction protocol between the server and the client.

Routes

Compojure route definitions are just functions that accept request maps and return response maps. Each route is an HTTP method paired with a URL-matching pattern, an argument list, and a body.

(GET "/" [] "Show something")
(POST "/" [] "Create something")
(PUT "/" [] "Replace something")
(PATCH "/" [] "Modify Something")
(DELETE "/" [] "Annihilate something")
(OPTIONS "/" [] "Appease something")
(HEAD "/" [] "Preview something")

The body may be a function, that must accept the request as a parameter:

(GET "/" [] (fn [req] "Do something with req"))

Or, we can just use the request directly by declaring it as the second argument to the route:

(GET "/foo" request (interpose ", " (keys request)))

The above route reads out all the keys from the request map and displays them. The output will look like the following:

:ssl-client-cert, :remote-addr, :scheme, :query-params, :session, :form-params,
:multipart-params, :request-method, :query-string, :route-params, :content-type,
:cookies, :uri, :server-name, :params, :headers, :content-length, :server-port,
:character-encoding, :body, :flash

Route patterns may include named parameters:

(GET "/hello/:name" [name] (str "Hello " name))

We can adjust what each parameter matches by supplying a regex:

(GET ["/file/:name.:ext" :name #".*", :ext #".*"] [name ext]
    (str "File: " name ext))

Handlers may utilize query parameters:

(GET "/posts" []
  (fn [req]
    (let [title (get (:params req) "title")
          author (get (:params req) "title")]
      " Do something with title and author")))

Or, for POST and PUT requests, form parameters:

(POST "/posts" []
  (fn [req]
    (let [title (get (:params req) "title")
          author (get (:params req) "title")]
      "Do something with title and author")))

Compojure also provides syntax sugar for accessing the form parameters as seen below:

(POST "/hello" [id] (str "Welcome " id))

In the guestbook application example we saw the following route defined:

(POST "/"  [name message] (save-message name message))

This route extracts the name and the message form parameters and binds them to variables of the same name. We can now use them as any other declared variable.

It's also possible to use the regular Clojure destructuring inside the route.

(GET "/:foo" {{foo "foo"} :params}
  (str "Foo = " foo))

Furthermore, Compojure allows destructuring a subset of form parameters and creating a map from the rest.

[x y & z]
x -> "foo"
y -> "bar"
z -> {:v "baz", :w "qux"}

Above, parameters x and y have been bound to variables, while parameters v and w remain in a map called z. Finally, if we need to get at the complete request along with the parameters we can do the following:

(GET "/" [x y :as r] (str x y r))

Here we bind the form parameters x an y, and bind the complete request map to the variable r.

Return values

The return value of a route block determines at least the response body passed on to the HTTP client, or at least the next middleware in the ring stack. Most commonly, this is a string, as in the above examples. But, we may also return a response map:

(GET "/" []
    {:status 200 :body "Hello World"})

(GET "/is-403" []
    {:status 403 :body ""})

(GET "/is-json" []
    {:status 200 :headers {"Content-Type" "application/json"} :body "{}"})

Static Files

To serve up static files, use compojure.route.resources. Resources will be served from your project's resources/ folder.

(require '[compojure.route :as route])

(route/resources "/")) ; Serve static resources at the root path

Organizing application routes

It's a good practice to organize your application routes together by functionality. Compojure provides the defroutes macro which can group several routes together and bind them to a symbol.

(defroutes auth-routes
  (POST "/login" [id pass] (login id pass))
  (POST "/logout" [] (logout)))

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

It's also possible to group routes by common path elements using context. If you had a set of routes that all shared /user/:id path as seen below:

(defroutes user-routes
  (GET "/user/:id/profile" [id] ...)
  (GET "/user/:id/settings" [id] ...)
  (GET "/user/:id/change-password [id] ...))

We could rewrite that as:

(def user-routes
  (context "/user/:id" [id]
    (GET "/profile" [] ...)
    (GET "/settings" [] ...)
    (GET "/change-password" [] ...)))

Once all your application routes are defined you can add them to the main handler of your application. You'll notice that the template already defined the app in the handler namespace of your application. All you have to do is add your new routes there.

(def app
  (-> (routes
        home-routes
        base-routes)
      development-middleware
      production-middleware))

Further documentation is available on the official Compojure wiki

Restricting access

Some pages should only be accessible if specific conditions are met. For example, you may wish to define admin pages only visible to the administrator, or a user profile page which is only visible if there is a user in the session.

Using the buddy.auth.accessrules namespace from Buddy, we can define rules for restricting access to specific pages.

Specifying Access Rules

Let's take a look at how to create a rule to specify that restricted routes should only be accessible if the :user key is present in the session.

First, we'll reference several Buddy namespaces in the <app>.middleware namespace.

(ns myapp.middleware
  (:require ...
            [buddy.auth.middleware :refer [wrap-authentication]]
            [buddy.auth.accessrules :refer [wrap-access-rules]]
            [buddy.auth.backends.session :refer [session-backend]]
            [buddy.auth :refer [authenticated?]]))

Next, we'll create the access rules for our routes. The rules are defined using a vector where each rule represented using a map. A simple rule that checks whether the user has been authenticated can be seen below.

(def rules
  [{:uri "/restricted"
    :handler authenticated?}])

We'll also define an error handler function that will be used when access to a particular route is denied:

(defn on-error
  [request value]
  {:status 403
   :headers {}
   :body "Not authorized"})

Finally, we have to add the necessary middlware to enable the access rules and authentication using a session backend.

(defn production-middleware [handler]
  (println "initializing")
  (-> handler
      (wrap-access-rules {:rules rules :on-error on-error})
      (wrap-authentication (session-backend))
      ...))

Note that the order of the middleware matters and wrap-access-rules must precede wrap-authentication.

Buddy session based authentication is triggered by setting the :identity key in the session when the user is successfully authenticated.

(def user {:id "bob" :pass "secret"})

(defn login! [{:keys [params session]}]
  (when (= user params)
    (-> "ok"
        response
        (content-type "text/html")
        (assoc :session (assoc session :identity "foo")))))