Fork me on GitHub

Do I have to re-eval the router definition every time I change my handler?

(def app
    [["/" main-page] 
     ["/config" {:post config/save}]]) ; config/save cannot be used as var here and thus you need to re-eval `app` every time


@jumar it doesn’t work with Var? It could, and there is a multimethod to handle how things are expanded, Var could be added as default mapping to it.


With var I get this error:

No implementation of method: :expand of protocol: #'reitit.core/Expand found for class: clojure.lang.Var


(extend-protocol reitit.core/Expand
  (expand [this _] {:handler this}))


does that help?


Yes! You are super-helpful, thanks! Is it worth filing an issue or should I just keep it in my code?


I think it could do that out-of-the-box, so PR welcome

👍 3

you could also re-create the top-level ring handler for all requests in dev. quite slow, but always up-to-date, e.g. (defn app [request] ((ring/…) request)). Personally using integrant-repl and have keyboard mapping of shift + Cmd + ö to run reset. takes few millis so, just that.


I try to avoid using tools.namespace refresh (if that's the thing integrant uses) - at least when changes are small. Reloading the app (I do that now) is easy when it's in the same namespace but since config/save is another ns it's more annoying 🙂.

David Pham15:01:46

I know it might not be the best place to ask this, but I am trying to run the example from Reitit+Malli, and I can create a post request with httpie, but I failed to make the same request with http-kit

David Pham15:01:01

Did anyone go through the same trouble?

David Pham15:01:19

(ns test.server
   [org.httpkit.client :as client]
   [cheshire.core :as j]))

;; you can use babashka for this example 

  {:accept :json
   :body (j/encode {:x 1 :y 20}) :as :text})

;; => {"schema":"[:map [:x any?] [:y any?]]","errors":[{"in":[],"value":null,"message":"invalid type","schema":"[:map [:x any?] [:y any?]]","path":[],"type":"malli.core/invalid-type"}],"value":null,"type":"reitit.coercion/request-coercion","coercion":"malli","in":["request","body-params"],"humanized":["invalid type"]}

David Pham15:01:19

The funny thing is this works fine

David Pham15:01:45

http POST :3000/math/plus x:=1 y:=20


The content type isn't being set on the http-kit client, whereas in httpie, it's set automatically as application/json


might that have anything to do with it?

David Pham15:01:00

Let me double check

David Pham15:01:38

nope, it it is not that 😕

David Pham15:01:08

It drives me crazy haha

David Pham15:01:51

I am fairly certain it has to play with the coercion, because when I just make an echo of the request, I do see a json body.


can you share your schema please?


and your router setup please

David Pham15:01:10

   ;; Uncomment to use
   ;; [ :as dev]
   ;; [spec-tools.spell :as spell]
   [reitit.swagger :as swagger]
   [clojure.core.async :as a]
   [ :as io]
   [clojure.pprint :as pprint]
   [integrant.core :as ig]
   [io.pedestal.http :as server]
   [muuntaja.core :as m]
   [malli.util :as mu]
   [ :as pretty]
   [reitit.http :as http]
   [reitit.http.coercion :as coercion]
   [reitit.http.interceptors.exception :as exception]
   [reitit.http.interceptors.multipart :as multipart]
   [reitit.http.interceptors.muuntaja :as muuntaja]
   [reitit.http.interceptors.parameters :as parameters]
   [reitit.pedestal :as pedestal]
   [reitit.ring :as ring]
   [reitit.swagger-ui :as swagger-ui]))

(defn interceptor [x]
  {:enter (fn [ctx] (update-in ctx [:request :via] (fnil conj []) {:enter x}))
   :leave (fn [ctx] (update-in ctx [:response :body] conj {:leave x}))})

(def routes
    {:get {:handler
           (fn [context] (pprint/pprint context)
             {:status 200
              :body (slurp (:body context))})}
     :post {:handler
            (fn [context] (pprint/pprint context)
              {:status 200
               :body (slurp (:body context))})}}]

    {:get {:no-doc true
           {:info {:title "my-api"
                   :description "with [malli]() and reitit-ring"}
            :tags [{:name "files", :description "file api"}
                   {:name "math", :description "math api"}]}
           :handler (swagger/create-swagger-handler)}}]

    {:swagger {:tags ["math"]}
     #_#_:interceptors [(interceptor :api)]}

     {:get {:summary "plus with malli query parameters"
            :parameters {:query [:map [:x int?] [:y int?]]}
            :responses {200 {:body [:map [:total int?]]}}
            :handler (fn [{{{:keys [x y]} :query} :parameters}]
                       {:status 200
                        :body {:total (+ x y)}})}

      :post {:summary "plus with malli body parameters"
             :parameters {:body [:map [:x int?] [:y int?]]}
             ;; :responses {200 {:body [:map [:total int?]]}}
             :handler (fn [{{{:keys [x y]} :body} :parameters :as context}]
                        (pprint/pprint context)
                        {:status 200
                         :body {:total (+ x y)}})}}]]])

(def router-args
  {;:reitit.interceptor/transform dev/print-context-diffs ;; pretty context diffs
   ;;:validate spec/validate ;; enable spec validation for route data
   ;;:reitit.spec/wrap spell/closed ;; strict top-level validation
   :exception pretty/exception
   {:coercion (reitit.coercion.malli/create {:compile mu/open-schema})
    :muuntaja m/instance
    [;; swagger feature
     ;; swagger/swagger-feature
     ;; ;; format
     ;; query-params & form-params
     ;; content-negotiation
     ;; decoding request body
     ;; encoding response body
     ;; exception handling
     ;; coercing response bodys
     ;; coercing request parameters
     ;; multipart

(defn router []
   (http/router routes router-args)
   ;; optional default ring handler (if no routes have matched)
     {:path "/"
      :config {:validatorUrl nil
               :operationsSorter "alpha"}})

(defn start [{:keys [port router]}]
  (let [service-map (-> {:env :dev
                         ::server/type :jetty
                         ::server/port (or port 3000)
                         ::server/join? false
                         ;; no pedestal routes
                         ::server/routes []
                         ;; allow serving the swagger-ui styles & scripts from self
                          {:default-src "'self'"
                           :style-src "'self' 'unsafe-inline'"
                           :script-src "'self' 'unsafe-inline'"}}}
                        ;; use the reitit router
                        (pedestal/replace-last-interceptor router)
    (server/start service-map)

(def system-config
  {::jetty   {:port    3000
              :join?   false
              :router (ig/ref ::router)}
   ::router {}})

(defmethod ig/init-key ::jetty [_ {:keys [port join? router]}]
  (println "server running in port" port)
  (start {:port port :join? join? :router router}))

(defmethod ig/halt-key! ::jetty [_ service-map]
  (println "Stopping" service-map)
  (server/stop service-map))

(defmethod ig/init-key ::router [_ _]

(defn -main []
  (ig/init system-config))


  {:headers {"Content-Type" "application/json"}
   :body (j/encode {:x 1 :y 20})})


For some reason, it doesn't support setting the :content-type directly


❯ cat foo.clj                                              
(ns foo
   [org.httpkit.client :as client]
   [cheshire.core :as j]))
;; you can use babashka for this example
  {:headers {"Content-Type" "application/json"}
   :body (j/encode {:x 1 :y 20})})

  ~                                                                                                                       
❯ bb foo.clj 

David Pham15:01:54

I am sorry for the trouble...


not a problemo

Steven Deobald16:01:04

@gupta.harsh96 Small world. 🙂 If you're evaluating reitit, fwiw, I fully endorse.

😀 3
Harsh Gupta17:01:30

Thanks! reitit, is super cool 😄