REST methods in forms with Kit, Reitit, and Ring

Regular HTML only supports two of the five REST methods. To my knowledge Kit does not come with any functionality that would allow you to directly call other methods without using some front-end shenanigans. If you want to avoid that, your other option is a reitit.ring middleware.

Let’s say you want to add a button that makes a DELETE request, removing an item. For it to work, you need:

  • The button itself, wrapped in a form. This form must use the “POST” method (others are not available in pure HTML), and contain a hidden field called “_method”. Its value should be set to “DELETE”.
  • An appropriate route and controller to handle the deletion (no surprise here)
  • A middleware that will allow you to swap the request method on the fly if the form contains a value in the hidden field.

This solution comes from the reitit documentation, I just added what’s necessary for it to work with Kit.

Button

Inside your HTML template, add a button wrapped in a form like below:

<form method="post" action="/delete/{{id}}">
  {% csrf-field %}
  <input type="hidden" name="_method" value="delete" />
  <input type="submit" class="button" value="Delete" />
</form>

The csrf-field is required for the form to be able to post successfully. Notice the hidden field with name="_method" and value="delete". This is important because that’s the field we’ll use in the middleware to swap the request method.

Route and controller

You now need to add the route in the pages.clj file.

["/delete/:id" {:delete item/delete!}]

The controller should end up in the appropriate web/controllers file (this one is just a placeholder):

(defn delete! [{:keys [path-params] :as request}]
  (clojure.pprint/pprint "DELETE ACTION CALLED!")
  (http-response/found "/"))

Middleware

Finally, the most important piece of the puzzle. Add the implementation of the middleware (for example from reitit documentation) to your web/middleware/core.clj file:

(defn- hidden-method
  [request]
  (some-> (or (get-in request [:form-params "_method"])
              (get-in request [:multipart-params "_method"])) 
          clojure.string/lower-case
          keyword))

(def wrap-hidden-method
  {:name ::wrap-hidden-method
   :wrap (fn [handler]
           (fn [request]
             (if-let [fm (and (= :post (:request-method request))
                              (hidden-method request))]
               (handler (assoc request :request-method fm))
               (handler request))))})

This middleware checks whether your request contains the _method field. If it does, whatever is in that field will be converted into a request method. In this case, the value of the _method field is “delete”. The middleware converts it to “:delete” and places it in the request map.

The next step is to make sure that the middleware is called at the right time. Add it to the ring/ring-handler definition in web/handler.clj:

(defmethod ig/init-key :handler/ring
  [_ {:keys [router api-path] :as opts}]
  (ring/ring-handler
    router
    (ring/routes
      (ring/create-resource-handler {:path "/"})
      (when (some? api-path)
        (swagger-ui/create-swagger-ui-handler {:path api-path
                                               :url  (str api-path "/swagger.json")}))
      (ring/create-default-handler
        {:not-found
         (constantly {:status 404, :body "Page not found"})
         :method-not-allowed
         (constantly {:status 405, :body "Not allowed"})
         :not-acceptable
         (constantly {:status 406, :body "Not acceptable"})}))
    {:middleware [(middleware/wrap-base opts)
                  middleware/wrap-hidden-method]})) ;; <---- calling the middleware here

Your application should now correctly respond on the ["/delete/:id" {:delete item/delete!}] route.