RESTful Services

Contents

  1. Working with Swagger
  2. Authentication

The recommended way to write REST services in Luminus is by using the Reitit Swagger support.

The easiest way to add Swagger support is by using the +swagger profile:

lein new luminus swag +swagger

The resulting project will contain an <app>.routes.services namespace with a few example routes defined.

Working with Swagger

We can see that routes are declared in this namespace. The routes look similar to the default route definitions, except they declare additional metadata regarding the type of parameters they accept and the responses they returned. this metadata is used to generate the Swagger UI test page for the services, validate requests and responses, and handle coercion.

["/plus"
     {:get {:summary "plus with spec query parameters"
            :parameters {:query {:x int?, :y int?}}
            :responses {200 {:body {:total pos-int?}}}
            :handler (fn [{{{:keys [x y]} :query} :parameters}]
                       {:status 200
                        :body {:total (+ x y)}})}
      :post {:summary "plus with spec body parameters"
             :parameters {:body {:x int?, :y int?}}
             :responses {200 {:body {:total pos-int?}}}
             :handler (fn [{{{:keys [x y]} :body} :parameters}]
                        {:status 200
                         :body {:total (+ x y)}})}}]]

The above service operation can be called from ClojureScript as follows:

(ns swag.core
  (:require [reagent.core :as reagent :refer [atom]]
            [ajax.core :refer [GET]]))

(defn add [params result]
  (GET "/api/plus"
       {:headers {"Accept" "application/transit+json"}
        :params @params
        :handler #(reset! result %)}))

(defn int-value [v]
  (-> v .-target .-value int))

(defn home-page []
  (let [params (atom {})
        result (atom nil)]
    (fn []
      [:div
       [:form
        [:div.form-group
         [:label "x"]
         [:input
          {:type :text
           :on-change #(swap! params assoc :x (int-value %))}]]
        [:div.form-group
         [:label "y"]
         [:input
          {:type :text
           :on-change #(swap! params assoc :y (int-value %))}]]]
       [:button.btn.btn-primary {:on-click #(add params result)} "Add"]
       (when @result
         [:p "result: " @result])])))

(reagent/render-component [home-page] (.getElementById js/document "app"))

The project is also setup to generate a documentation page for the service. The API documentation is available at the swagger.json and /swagger-ui URIs declared at the top of the service-routes route group:

["/swagger.json"
     {:get (swagger/create-swagger-handler)}]

["/api-docs/*"
 {:get (swagger-ui/create-swagger-ui-handler
         {:url "/api/swagger.json"
          :config {:validator-url nil}})}]]

Authentication

Services can have their own authentication rules. This is useful if you wish to return different kinds of errors than you would when serving HTML pages.

In order to provide authentication, we'll first need to implement wrap-restricted middleware:

(def wrap-restricted
  {:name :wrap-restricted
   :wrap (fn wrap-restricted [handler]
           (fn [req]
             (if (boolean (:identity req))
               (handler req)
               (unauthorized
{:error "You are not authorized to perform that action."}))))})

Next, we can create a restricted context:

(defn service-routes []
  ["/api"
   {:coercion   schema-coercion/coercion
    :muuntaja   formats/instance
    :swagger    {:id ::api}
    :middleware [...]}
  ...
  ["/restricted"
      {:swagger    {:tags ["restricted"]}
       :middleware [wrap-restricted]}
   ...])

Any routes with /restricted prefix will now require authentication. We can now define services as follows:

(defn admin? [req]
  (and (authenticated? req)
       (#{:admin} (:role (:identity req)))))

(defn service-routes []
  ["/api"
   {:coercion   schema-coercion/coercion
    :muuntaja   formats/instance
    :swagger    {:id ::api}
    :middleware [...]}
  ["/login"
   {:post
    {:summary    "handles user login"
     :parameters {:body {:email s/Str
                         :pass  s/Str}}
     :responses  {200 {:body auth/LoginResponse}}
     :handler    (fn [{{{:keys [email pass]} :body} :parameters :as req}]
                   (assoc-in (ok {:userid userid}) [:session :identity] {:userid userid}))}}]

  ["/logout"
   {:post
    {:summary   "remove the user from the session"
     :responses {200 {:body auth/LogoutResponse}}
     :handler (fn [_] (assoc (ok "ok") :session nil))}}]
  ["/restricted"
      {:swagger    {:tags ["restricted"]}
       :middleware [wrap-restricted]}
   ["/user" {:get (fn [request] (ok (-> request :session :identity)))}]]])

In the above example, the /login route does not require authentication. Meanwhile, the routes defined within the /api context will only be accessible when a user is present in the session.