reitit

ppandis 2025-07-08T12:46:30.868699Z

Hello, I'm trying out reittit and I'm a bit confused. Is it necessary to use it with ring? Can you define your routes using just reitit.core/router and then serve them with an httpkit server? I'm not against ring but I'm trying to test thing one step at a time.

Kirill Chernyshov 2025-07-08T12:56:17.233729Z

ring in reitit documentation refers to ring application, not ring web server. You can use any as long as it is compatible with ring application. httpkit is compatible.

ppandis 2025-07-08T13:02:57.819079Z

Thank you @delaguardo. So, to understand. Is there any server that would support running an app made of just a reitit router? What is the purpose of the standalone core router (as in not wrapped as a ring router) in real life?

Kirill Chernyshov 2025-07-08T13:14:20.464669Z

I'm not aware of such server. But router itself is useful not only in the context of serving http responses. For example to handle messages in a queue, mimic defmulti with enriched context, etc.

1
ppandis 2025-07-08T18:39:23.339849Z

I am trying to read either query or body parameters for a few hours now and I'm at a loss as to what I'm doing wrong. Is there anything obviously wrong with this?

(ns xyz.core
  (:require [org.httpkit.server :as s]
            [reitit.ring :as ring]
            [reitit.ring.coercion :as rrc]
            [reitit.coercion.schema] 
            [schema.core :as schema])
  (:gen-class))

(defn hello-ctrl [req]
  (let [name (-> req :parameters :path :name)
        age (-> req :parameters :query :age)]
    (println req)
    {:status  200
     :headers {"Content-Type" "text/html"}
     :body    (str "Hello " name " :)\nAre you " age " years old?")}))

(defn default-handler [_]
  {:status  200
   :headers {"Content-Type" "text/html"}
   :body    "It's OK, yes!"})

(def app2
  (ring/ring-handler
   (ring/router
    [["/" {:get default-handler}]
     ["/hello/:name" {:name ::yolo
                      :get {:handler hello-ctrl
                            :parameters {:path {:name schema/Str}
                                         :query {:age schema/Int}}}}]]
    {:data {:coercion reitit.coercion.schema/coercion
            :middleware [rrc/coerce-request-middleware]}})))
Stack trace in thread

ppandis 2025-07-10T16:07:09.347379Z

I actually figured out how to use muuntaja eventually to parse the body and how one needs to enable it etc.

dharrigan 2025-07-10T16:11:53.117579Z

👍 🎉

ppandis 2025-07-08T18:39:39.309749Z

The stack trace

; Tue Jul 08 19:32:46 BST 2025 [http-kit-server-worker-4] ERROR - GET /hello/nick
; clojure.lang.ExceptionInfo: Request coercion failed {:schema {:age Int, Keyword Any}, :errors (not (map? [nil])), :type :reitit.coercion/request-coercion, :coercion #Coercion{:name :schema}, :value nil, :in [:request :query-params], :request {:reitit.core/match #reitit.core.Match{:template "/hello/:name", :data {:coercion #Coercion{:name :schema}, :middleware [{:name :reitit.ring.coercion/coerce-request, :spec :reitit.spec/parameters, :compile #function[reitit.ring.coercion/fn--12869]}], :name :xyz.core/yolo, :get {:handler #function[xyz.core/hello-ctrl], :parameters {:path [{:name java.lang.String}], :query [{:age Int}]}}}, :result #reitit.ring.Methods{:get #reitit.ring.Endpoint{:data {:coercion #Coercion{:name :schema}, :middleware [{:name :reitit.ring.coercion/coerce-request, :spec :reitit.spec/parameters, :compile #function[reitit.ring.coercion/fn--12869]}], :name :xyz.core/yolo, :handler #function[xyz.core/hello-ctrl], :parameters {:path {:name java.lang.String}, :query {:age Int}}}, :handler #function[reitit.ring.coercion/fn--12869/fn--12871/fn--12872], :path "/hello/:name", :method :get, :middleware [#reitit.middleware.Middleware{:name :reitit.ring.coercion/coerce-request, :wrap #function[reitit.ring.coercion/fn--12869/fn--12871], :spec :reitit.spec/parameters}]}, :head nil, :post nil, :put nil, :delete nil, :connect nil, :options #reitit.ring.Endpoint{:data {:coercion #Coercion{:name :schema}, :middleware [{:name :reitit.ring.coercion/coerce-request, :spec :reitit.spec/parameters, :compile #function[reitit.ring.coercion/fn--12869]}], :name :xyz.core/yolo, :no-doc true, :handler #function[reitit.ring/fn--12600/fn--12609]}, :handler #function[reitit.ring/fn--12600/fn--12609], :path "/hello/:name", :method :options, :middleware [#reitit.middleware.Middleware{:name :reitit.ring.coercion/coerce-request, :wrap nil, :spec :reitit.spec/parameters}]}, :trace nil, :patch nil}, :path-params {:name "nick"}, :path "/hello/nick"}, :reitit.core/router #object[reitit.core$mixed_router$reify__11754 0x70dbf017 "reitit.core$mixed_router$reify__11754@70dbf017"], :remote-addr "0:0:0:0:0:0:0:1", :start-time 932866731487291, :headers {"accept" "*/*", "host" "localhost:9393", "user-agent" "curl/8.7.1"}, :async-channel #object[org.httpkit.server.AsyncChannel 0x74e46ad4 "/[0:0:0:0:0:0:0:1]:9393<->/[0:0:0:0:0:0:0:1]:57012"], :server-port 9393, :content-length 0, :websocket? false, :content-type nil, :character-encoding "utf8", :uri "/hello/nick", :server-name "localhost", :query-string "age=37", :path-params {:name "nick"}, :body nil, :scheme :http, :request-method :get}}
; 	at reitit.coercion$request_coercion_failed_BANG_.invokeStatic(coercion.cljc:51)
; 	at reitit.coercion$request_coercion_failed_BANG_.invoke(coercion.cljc:49)
; 	at reitit.coercion$request_coercer$fn__12142.invoke(coercion.cljc:105)
; 	at reitit.coercion$coerce_request$fn__12182.invoke(coercion.cljc:142)
; 	at clojure.lang.PersistentArrayMap.kvreduce(PersistentArrayMap.java:475)
; 	at clojure.core$fn__8559.invokeStatic(core.clj:6987)
; 	at clojure.core$fn__8559.invoke(core.clj:6967)
; 	at clojure.core.protocols$fn__8287$G__8282__8296.invoke(protocols.clj:174)
; 	at clojure.core$reduce_kv.invokeStatic(core.clj:6998)
; 	at clojure.core$reduce_kv.invoke(core.clj:6989)
; 	at reitit.coercion$coerce_request.invokeStatic(coercion.cljc:140)
; 	at reitit.coercion$coerce_request.invoke(coercion.cljc:139)
; 	at reitit.ring.coercion$fn__12869$fn__12871$fn__12872.invoke(coercion.cljc:40)
; 	at reitit.ring$ring_handler$fn__12708.invoke(ring.cljc:386)
; 	at 
I really don't know how to read this Request coercion failed {:schema {:age Int, Keyword Any}, :errors (not (map? [nil])) My call is curl -v GET ""

2025-07-08T19:05:15.911579Z

> I really don't know how to read this Just to answer this part, the error is saying it found the value [nil] where it expected a keyword map.

ppandis 2025-07-08T20:27:24.851309Z

Right, I've even copy pasted the example from here https://cljdoc.org/d/metosin/reitit/0.9.1/doc/ring/pluggable-coercion and it works if called programmatically but trying to use curl or httpie it fails with the above exception. My curl is

curl -v POST -H "Content-type: text/plain" -d "y=6" ""
or httpie
http POST "" y=5
I've tried to include ring wrap-params as my first middleware but no difference.

dharrigan 2025-07-09T07:53:20.049269Z

Which example did you use, as there are several on the page.

ppandis 2025-07-09T07:54:21.295869Z

The one under Full Example

(require '[reitit.ring.coercion :as rrc])
(require '[reitit.coercion.schema])
(require '[reitit.ring :as ring])
(require '[schema.core :as s])

(def PositiveInt (s/constrained s/Int pos? 'PositiveInt))

(def app
  (ring/ring-handler
    (ring/router
      ["/api"
       ["/ping" {:name ::ping
                 :get (fn [_]
                        {:status 200
                         :body "pong"})}]
       ["/plus/:z" {:name ::plus
                    :post {:coercion reitit.coercion.schema/coercion
                           :parameters {:query {:x s/Int}
                                        :body {:y s/Int}
                                        :path {:z s/Int}}
                           :responses {200 {:body {:total PositiveInt}}}
                           :handler (fn [{:keys [parameters]}]
                                      (let [total (+ (-> parameters :query :x)
                                                     (-> parameters :body :y)
                                                     (-> parameters :path :z))]
                                        {:status 200
                                         :body {:total total}}))}}]]
      {:data {:middleware [rrc/coerce-exceptions-middleware
                           rrc/coerce-request-middleware
                           rrc/coerce-response-middleware]}})))

ppandis 2025-07-09T07:56:21.677159Z

I've tried modifying my curl Content-type and got to see the body value as a param using the middleware logger but still I have not managed to use the :body as whatever I do this key is not generated.

ppandis 2025-07-09T07:59:09.341829Z

I can see y as a form param though using the default "application/x-www-form-urlencoded"

; --- :request :reitit.ring.middleware.parameters/parameters ---
; 
;   {:async-channel #<org.httpkit.server.AsyncChannel@64934162 /[0:0:0:0:0:0:0:1]:9393<->/[0:0:0:0:0:0:0:1]:60487>,
;    :body #<org.httpkit.BytesInputStream@64e1ea05 BytesInputStream[len=3]>,
;    :character-encoding "utf8",
;    :content-length 3,
;    :content-type "application/x-www-form-urlencoded",
;    :headers {"accept" "*/*",
;              "content-length" "3",
;              "content-type" "application/x-www-form-urlencoded",
;              "host" "localhost:9393",
;              "user-agent" "curl/8.7.1"},
;    :path-params {:z "2"},
;    :query-string "x=34",
;    :remote-addr "0:0:0:0:0:0:0:1",
;    :request-method :post,
;    :scheme :http,
;    :server-name "localhost",
;    :server-port 9393,
;    :start-time 946790008908541,
;    :uri "/api/plus/2",
;    :websocket? false,
;    +:form-params {"y" "6"},
;    +:params {"x" "34", "y" "6"},
;    +:query-params {"x" "34"}}

dharrigan 2025-07-09T08:06:15.097549Z

Here is a working version:

dharrigan 2025-07-09T08:06:24.448919Z

(ns xyz.core
  (:require
   [muuntaja.core :as m]
   [reitit.coercion.schema]
   [reitit.ring :as ring]
   [reitit.ring.coercion :as rrc]
   [ring.adapter.jetty9 :as jetty]
   [ring.middleware.params :as params]
   [schema.core :as schema])
  (:gen-class))

(defn hello-ctrl [req]
  (let [name (-> req :parameters :path :name)
        age (-> req :parameters :query :age)]
    (println req)
    {:status  200
     :headers {"Content-Type" "text/html"}
     :body    (str "Hello " name " :)\nAre you " age " years old?")}))

(defn default-handler [_]
  {:status  200
   :headers {"Content-Type" "text/html"}
   :body    "It's OK, yes!"})

(defn app2
  []
  (ring/ring-handler
   (ring/router
    [["/" {:get default-handler}]
     ["/hello/:name" {:name ::yolo
                      :get {:handler hello-ctrl
                            :parameters {:path {:name schema/Str}
                                         :query {:age schema/Int}}}}]]
    {:data {:muuntaja m/instance
            :coercion reitit.coercion.schema/coercion
            :middleware [params/wrap-params
                         rrc/coerce-request-middleware]}})))

(defn go
  []
  (jetty/run-jetty (fn [request] ((app2) request)) {:port 3001 :allow-null-path-info true :join? false}))

dharrigan 2025-07-09T08:06:33.203869Z

❯ curl \?age\=30
Hello nick :)
Are you 30 years old?%  

dharrigan 2025-07-09T08:07:19.903759Z

A few things:

dharrigan 2025-07-09T08:07:37.234869Z

You need ring to decode the params, that's what the params middleware is about

dharrigan 2025-07-09T08:08:00.936489Z

(however, you could, instead use this: [reitit.ring.middleware.parameters :as parameters])

dharrigan 2025-07-09T08:08:11.234619Z

(I use that, which just wraps rings params)

dharrigan 2025-07-09T08:08:48.564479Z

:middleware [
....
                        muuntaja/format-middleware
                        parameters/parameters-middleware
....]
...
...
...

dharrigan 2025-07-09T08:09:19.416889Z

Secondly, for repl usage, I wrap app2 into a function, so I can re-eval the form and not have to start/restart the server.

dharrigan 2025-07-09T08:09:20.392129Z

.

dharrigan 2025-07-09T08:09:52.063899Z

dharrigan 2025-07-09T08:09:53.146999Z

.

dharrigan 2025-07-09T08:09:55.663069Z

Hope that helps!

dharrigan 2025-07-09T08:10:15.467469Z

Have a look here:

dharrigan 2025-07-09T08:10:26.259709Z

dharrigan 2025-07-09T08:11:04.499519Z

.

ppandis 2025-07-09T08:11:55.955419Z

Right, in between copy-pasta I forgot to post my latest version which was using

{:data {:middleware [reitit.ring.middleware.parameters/parameters-middleware
                         ring.middleware.keyword-params/wrap-keyword-params
                         rrc/coerce-request-middleware
                         rrc/coerce-response-middleware
                         rrc/coerce-exceptions-middleware
                         ]}
but I've missed the wrap-params . For reloading I use the wrap-reload middleware in a dev namespace where I launch the server. I'll need to update my code and test. Thank you!

dharrigan 2025-07-09T08:12:13.732029Z

you're most welcome! 🙂 Good luck! 🙂

dharrigan 2025-07-09T08:12:14.180659Z

.

ppandis 2025-07-09T11:54:14.084959Z

Sorry @dharrigan but is what you mean above that I could use

reitit.ring.middleware.parameters :as parameters
in place of
ring.middleware.params/wrap-params
? As, I did that, reading the reitit docs and how the first wraps the second and it did not works for me. Now using the second one I can see the body getting parsed but I still get a complain and I'll need to figure out what is the issue.

Akimbo 2025-07-08T04:26:42.839659Z

For some reason I can't get Ring + Reitit to parse POST requests as Json. Does something look wrong with this?

(def routes

  (reitit.ring/ring-handler
   (reitit.ring/router
    [["/" {:get home-page}]
     ["/authenticate/login" {:post auth/login-route
                             :data {:middleware [wrap-json-body]}}]]
    {:data {:middleware [format-middleware]
            :muuntaja muuntaja/instance}})
   (ring/create-default-handler)))

valerauko 2025-07-08T12:24:56.225329Z

does the request have content-type: application/json; charset=utf-8 header?