reitit

Andrew Leverette 2025-03-07T01:49:20.505349Z

Hello! I'm new to reitit, but I'm trying to build an application in Clojure with a reitit API. I'm trying to set up path parameter validation for a get route. The route has the form of resource/:uuid, and I basically just want to ensure that the path parameter is a uuid and if not return a 400 with an appropriate error code and message to the client. I think what I have so far is actually validating the path parameter, but a 500 is being thrown, so I'm not 100% sure about that. Mostly, I would love some guidance in getting pointed in the right direction with this. If there are any great resources that talk about this, I would appreciate the recommendations.

juhoteperi 2025-03-07T09:02:42.387789Z

the exception-middleware can take options on how to handle specific error types

juhoteperi 2025-03-07T09:02:58.712159Z

:reitit.coercion/request-coercion

juhoteperi 2025-03-07T09:05:30.484069Z

Can on the malli side you can control those property messages with :error/message or :error/fn: https://github.com/metosin/malli/?tab=readme-ov-file#custom-error-messages

jussi 2025-03-07T10:10:20.718169Z

We are using the Malli library with reitit and we validate that a path parameter is a uuid as follows

["/document/:id"
       {:get {:summary "Get document by id"
              :responses {200 {:content {:default {:schema #'schema/document}}}}
              :parameters {:path [:map [:id uuid?]]}
              :handler (fn [{:keys [path-params authorized-person]}]
                         {:status 200
                          :body (document/get-by-id db/node
                                                    (parse-uuid (:id path-params))
                                                    (:id authorized-person))})}}]

juhoteperi 2025-03-07T10:11:35.331459Z

:uuid and uuid? predicate are about the same thing (but not exactly)

jussi 2025-03-07T10:17:04.335779Z

That is true, missed that :uuid definition in the second example 👀

jussi 2025-03-07T10:20:59.419409Z

Doesn't :uuid schema use uuid? fn under the hood as a predicate? What is the "not exaclty" difference here?

juhoteperi 2025-03-07T10:25:19.218159Z

(some) schema properties only with with "simple-schemas" (i.e. the named version, not predicates)

juhoteperi 2025-03-07T10:25:20.503549Z

https://github.com/metosin/malli/issues/327

juhoteperi 2025-03-07T10:26:38.890189Z

And due to impl details with predicates (they are being stored as vars into the schema registry and then need to be checked for equality or something...) there are some special environments where predicate schemas don't work: https://github.com/metosin/malli/issues/556

juhoteperi 2025-03-07T10:27:11.083379Z

for example :string length :min :max properties are defined for :string schema

juhoteperi 2025-03-07T10:27:28.989549Z

From readme: NOTE: Predicate Schemas do not cover any schema properties, e.g. string? can't be modified with properties like :min and :max. If you want to use the schema properties, use real schema types instead, e.g. :string over string?.

jussi 2025-03-07T11:05:03.498299Z

Ah, makes sense, thanks!

Andrew Leverette 2025-03-07T01:50:15.038659Z

Here is the snippet with the router definition:

(defn- route->handler
  [app]
  {:health {:get health}
   :get-document {:handler (partial docs/get-document (:documents-repository app))
                  :parameters {:path DocumentId}}})

(defn- router
  [route-map]
  (ring/ring-handler
   (ring/router
    [base-api-url
     ["/v1"
      ["/health" (:health route-map)]
      ["/documents/:id" (:get-document route-map)]]]
    {:data {:coercion rcm/coercion
            :muuntaja (m/create
                       (assoc-in
                        m/default-options
                        [:formats "application/json" :encoder-opts]
                        {:encode-key-fn (comp csk/->camelCase name) :strip-nils true}))
            :middleware [muuntaja/format-middleware
                         coercion/coerce-request-middleware]}})))

Andrew Leverette 2025-03-07T03:59:30.571529Z

Okay, I'm making progress. Here is what I have now:

(defn- route->handler
  [app]
  {:health {:get health}
   :search-documents {:get (fn [_] {:status 200 :body "Searching documents"})}
   :get-document {:get (partial docs/get-document (:documents-repository app))
                  :parameters {:path {:id :uuid}}}})

(defn- router
  [route-map]
  (ring/ring-handler
   (ring/router
    [base-api-url
     ["/v1"
      ["/health" (:health route-map)]
      ["/search" ["/documents" (:search-documents route-map)]]
      ["/documents/:id" (:get-document route-map)]]]
    {:data {:coercion rcm/coercion
            :muuntaja (m/create
                       (assoc-in
                        m/default-options
                        [:formats "application/json" :encoder-opts]
                        {:encode-key-fn (comp csk/->camelCase name) :strip-nils true}))
            :middleware [muuntaja/format-middleware
                         coercion/coerce-exceptions-middleware
                         coercion/coerce-request-middleware]}})))
which does give me a 400 error with the response
{
  "value": {
    "id": "not-a-uuid"
  },
  "type": "reitit.coercion/request-coercion",
  "coercion": "malli",
  "in": [
    "request",
    "path-params"
  ],
  "humanized": {
    "id": [
      "should be a uuid"
    ]
  }
}

Andrew Leverette 2025-03-07T04:07:07.948789Z

I guess if I wanted to transform that error response into something else, I would either need to write my own coercion middleware, or write another middleware to take the response and transform it into the desired output.