reitit

eval-on-point 2025-10-24T02:23:51.607969Z

Is there a way to simply specify that a collection of routes share a response spec, such as a 401? It seems like the :parameters key accrues like you would expect, but the :responses key cannot because it is part of the data of a particular request method.

eval-on-point 2025-10-24T02:28:22.558959Z

Here is a rough sketch of what I am talking about:

["/pets" {}
 ["/dog" {:get {:handler (constantly {:status 200 :body "spot"})
                :responses {200 {:body #{"spot"}}
                            401 {:body #{"Unauthorized"}}}}}]
 ["/cat" {:get {:handler (constantly {:status 200 :body "fluffy"})
                :responses {200 {:body #{"fluffy"}}
                            401 {:body #{"Unauthorized"}}}}}]]
I want to configure this route tree so that all child methods of /pets/* may have a 401 response

opqdonut 2025-10-24T05:36:21.482049Z

Good question! I don't see any reason why the responses map couldn't get merged. I'll dig into the code.

opqdonut 2025-10-24T05:37:37.200959Z

Meanwhile, since the route tree is data, you could write a function that walks it and adds the necessary 401s ... if you feel that the added complexity is worth the guaranteed response schemas 🙂

opqdonut 2025-10-24T05:43:10.777519Z

This seems to work:

user> (def router (r/router ["/api" {:get {:responses {401 {:description "A default 401 response"}}}}
                             ["/foo" {:get {:handler identity
                                            :responses {200 {:description "A specific 200 response"}}}}]
                             ["/bar" {:get {:handler identity
                                            :responses {200 {:description "Another 200 response"}}}}]]))
#'user/router
user> (clojure.pprint/pprint (r/routes router))
[["/api/foo"
  {:get
   {:responses
    {401 {:description "A default 401 response"},
     200 {:description "A specific 200 response"}},
    :handler
    #object[clojure.core$identity 0x421c9c86 "clojure.core$identity@421c9c86"]}}]
 ["/api/bar"
  {:get
   {:responses
    {401 {:description "A default 401 response"},
     200 {:description "Another 200 response"}},
    :handler
    #object[clojure.core$identity 0x421c9c86 "clojure.core$identity@421c9c86"]}}]]

opqdonut 2025-10-24T05:43:42.766029Z

unfortunately you'll have to handle every method separately, I think

eval-on-point 2025-10-27T14:59:03.525419Z

The above caused me to re-investigate this. The accumulation of responses actually seems to work, but if you use the default route data specs, then route data validation fails. So, if you add a :responses key to the spec in your router options, everything seems to work. I am not sure if the ::reitit.ring.spec/data spec should be expanded to include an optional :responses key.

opqdonut 2025-10-28T06:20:06.881099Z

I'm not really following you. This seems to work:

user> (def router (r/router ["/api"
                             ["/foo" {:get {:handler identity
                                            :responses {200 {:description "A specific 200 response"}}}}]
                             ["/bar" {:post {:handler identity
                                             :responses {200 {:description "Another 200 response"}}}}]]
                            {:data {:get {:responses {401 {:description "A default 401 response"}}}
                                    :post {:responses {401 {:description "A default 401 response"}}}}
                             :validate reitit.spec/validate}))
#'user/router
user> (clojure.pprint/pprint (r/routes router))
[["/api/foo"
  {:get
   {:responses
    {401 {:description "A default 401 response"},
     200 {:description "A specific 200 response"}},
    :handler
    #object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"]},
   :post {:responses {401 {:description "A default 401 response"}}}}]
 ["/api/bar"
  {:get {:responses {401 {:description "A default 401 response"}}},
   :post
   {:responses
    {401 {:description "A default 401 response"},
     200 {:description "Another 200 response"}},
    :handler
    #object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"]}}]]
nil

opqdonut 2025-10-28T06:20:52.937129Z

No wait, I screwed up the validation...

opqdonut 2025-10-28T06:23:36.195339Z

No, it still passes even with reitit.ring.spec/validate, since the spec for :get (etc) is just map?

opqdonut 2025-10-28T06:26:11.676409Z

I never use validation myself so I might just be lost in the API here 😅

opqdonut 2025-10-28T06:30:19.801709Z

Well here we go:

opqdonut 2025-10-28T06:30:30.866309Z

user> (def router (reitit.ring/router ["/api"
                                       ["/foo" {:get {:handler identity
                                                      :responses {200 {:description "A specific 200 response"}}}}]
                                       ["/bar" {:post {:handler identity
                                                       :responses {200 {:description "Another 200 response"}}}}]]
                                      {:data {:get {:responses {401 {:description "A default 401 response"}}}
                                              :post {:responses {401 {:description "A default 401 response"}}}}
                                       :validate reitit.ring.spec/validate}))
Execution error (ExceptionInfo) at reitit.exception/exception (exception.cljc:19).
path "/api/foo" doesn't have a :handler defined for :post

{:path "/api/foo", :data {:responses {401 {:description "A default 401 response"}}}, :scope :post}

opqdonut 2025-10-28T06:31:23.926389Z

so using default route data doesn't work unless you have the same methods defined for every endpoint

eval-on-point 2025-10-24T13:45:27.293999Z

Thanks for the help. That does seem to fail if one of the child routes does not have a :get method, for example:

(ring/router ["/api" {:get {:responses {401 {:description "A default 401 response"}}}}
              ["/foo" {:put {:handler identity
                             :responses {200 {:description "A specific 200 response"}}}}]
              ["/bar" {:get {:handler identity
                             :responses {200 {:description "Another 200 response"}}}}]])
;; => Execution error (ExceptionInfo) at reitit.exception/exception (exception.cljc:19).
;;    path "/api/foo" doesn't have a :handler defined for :get
;;    {:path "/api/foo", :data {:responses {401 {:description "A default 401 response"}}}, :scope :get}
I think walking the tree may be the most elegant solution, but I think I will just make a helper function that takes method data and adds these common responses for our API. Thanks again!

opqdonut 2025-10-24T13:48:59.436159Z

Oh, right, good point about the missing methods. Maybe if reitit-ring was designed a bit differently you could have shared :responses next to :get instead of inside it...

eval-on-point 2025-10-24T13:58:05.050829Z

yeah I think that would be the best way to add support to this: allow a :responses field in the "route-data" that gets merged with the method data. I can make an issue to at least track this idea if you also agree.

valerauko 2025-10-26T11:53:04.038839Z

Are you sure it can't? I'd swear I have some "global" 401 definitions like that

opqdonut 2025-10-27T06:28:07.232629Z

nope:

user> (def router (r/router ["/api" {:responses {401 {:description "A default 401 response"}}}
                             ["/foo" {:get {:handler identity
                                            :responses {200 {:description "A specific 200 response"}}}}]
                             ["/bar" {:post {:handler identity
                                             :responses {200 {:description "Another 200 response"}}}}]]))
#'user/router
user> (clojure.pprint/pprint (r/routes router))
[["/api/foo"
  {:responses {401 {:description "A default 401 response"}},
   :get
   {:handler
    #object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"],
    :responses {200 {:description "A specific 200 response"}}}}]
 ["/api/bar"
  {:responses {401 {:description "A default 401 response"}},
   :post
   {:handler
    #object[clojure.core$identity 0x6bcd0f8b "clojure.core$identity@6bcd0f8b"],
    :responses {200 {:description "Another 200 response"}}}}]]

opqdonut 2025-10-27T06:28:11.561199Z

though maybe you have a trick that works?

opqdonut 2025-10-24T13:30:30.998579Z

Pushed out a new reitit release using an automated release workflow: 0.9.2-rc1. Testing welcome! If it seems fine, I'll make a proper release on monday.