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.