reitit

repository·master·Indexed 23 days ago

https://github.com/metosin/reitit

A fast, data-driven router for Clojure and ClojureScript supporting bi-directional routing and pluggable coercion via malli, schema, and clojure.spec. It provides specialized helpers for Ring, HTTP, Pedestal, and frontend routing through a modular architecture including reitit-core, reitit-ring, reitit-http, and tools for Swagger/OpenAPI documentation generation.

Tokens
50.6K
Snippets
147
Records
209
Agent score
81%

What's inside reitit

  1. Understand coercion middleware optimization and mounting

    master

    Reitit optimizes coercion middleware by compiling it against specific routes. The middleware is only mounted if the route explicitly defines :coercion along with either :parameters or :responses.

    If a route lacks these keys, the coercion middleware chain will be empty for that route, reducing overhead. You can verify the mounted middleware for a specific route by inspecting the router using reitit.core/match-by-name.

    (require '[reitit.core :as r])
    
    ;; Querying the compiled middleware chain for a route
    (-> (ring/get-router app)
        (r/match-by-name ::plus)
        :result :post :middleware
        (->> (mapv :name)))
    ; => [::mw/coerce-exceptions ::mw/coerce-request ::mw/coerce-response]
    
    ;; A route without coercion defined will have no mounted middleware
    (-> (ring/get-router app)
        (r/match-by-name ::ping)
        :result :get :middleware)
    ; => []
  2. Nest routers using a custom key

    master

    While reitit does not natively traverse nested routers stored in route data, you can implement nesting by storing a sub-router under a specific key (e.g., :router) within your route data.

    To make this work, you must implement a custom matching function that:

    1. Uses r/match-by-path to find a match in the top-level router.
    2. Checks if the match contains a sub-router in its :data.
    3. Recursively calls the matching logic using the sub-router and the remaining path segment.
    (require '[reitit.core :as r]
    (require '[clojure.string :as str])
    
    ;; 1. Define a router with nested routers in the data
    (def router
      (r/router
        [["/ping" :ping]
         ["/olipa/*" {:name :olipa
                      :router (r/router
                                [["/olut" :olut]
                                 ["/makkara" :makkara]
                                 ["/kerran/*" {:name :kerran
                                               :router (r/router
                                                         [["/avaruus" :avaruus]
                                                          ["/ihminen" :ihminen]])}]])]}]]))
    
    ;; 2. Implement recursive matching
    (defn recursive-match-by-path [router path]
      (when-let [match (r/match-by-path router path)]
        (if-let [subrouter (-> match :data :router)]
          (let [subpath (subs path (str/last-index-of (:template match) "/"))]
            (when-let [submatch (recursive-match-by-path subrouter subpath)]
              (cons match submatch)))
          (list match))))
    
    ;; Usage:
    ;; (recursive-match-by-path router "/olipa/kerran/avaruus")
  3. Use named schemas for reusability

    master

    OpenAPI supports reusable schema objects via $ref. Reitit automatically generates these reusable schema objects for Malli :refs and vars. This allows multiple endpoints to reference the same schema, which is also rendered in a dedicated section in Swagger UI.

    Note: As of version 0.7.2, reusable schema objects are not generated for Plumatic Schema or Spec.

  4. Use reitit.frontend.easy for simplified state management

    master

    Standard Reitit frontend routers require you to manually store and pass the router instance to every call.

    To simplify this, use the reitit.frontend.easy wrapper. It manages the router instance internally and automatically passes it to all calls, which is suitable for most applications since a browser typically only has one active event handler for page changes.

  5. Use nested controllers in route trees

    master

    When routes are nested, controllers are concatenated as route data is merged. This creates a hierarchical lifecycle:

    1. Root Controllers: Controllers defined at a parent route level start when any child route is navigated to.
    2. Child Controllers: Controllers defined at a specific route level start after the parent controllers have started.
    3. Transitions: If navigating between two child routes under the same parent, the parent controller remains active (its identity does not change), while the child controller is stopped and restarted if its identity (parameters) changes.

    Example structure:

    ["/" {:controllers [{:start (fn [_] (js/console.log "root start"))}]}
     ["/item/:id"
      {:controllers [{:parameters {:path [:id]}
                      :start (fn [parameters] (js/console.log "item start" (-> parameters :path :id)))
                      :stop (fn [parameters] (js/console.log "item stop" (-> parameters :path :id))))}]]
    ["/" {:controllers [{:start (fn [_] (js/console.log "root start"))}]}
     ["/item/:id"
      {:controllers [{:parameters {:path [:id]}
                      :start (fn [parameters] (js/console.log "item start" (-> parameters :path :id)))
                      :stop (fn [parameters] (js/console.log "item stop" (-> parameters :path :id))))}]]"
  6. Accumulate nested route data using meta-merge

    master

    In nested route trees, route data is accumulated from the root down to the leaves using meta-merge.

    • Default behavior: Collections are appended.
    • Overriding behavior: You can use metadata to change how data is merged. Use ^:prepend, ^:replace, or ^:displace on the target data to control the merge strategy.
    (def router
      (r/router
        ["/api" {:interceptors [::api]}
         ["/ping" ::ping]
         ["/admin" {:roles #{:admin}}
          ["/users" ::users]
          ["/db" {:interceptors [::db]
                  :roles ^:replace #{:db-admin}}]]]))
    
    ;; The /api/admin/db route will have :roles #{:db-admin} 
    ;; because ^:replace was used, overriding the parent's #{:admin}.
  7. How error formatting works in reitit

    master
    When creating a router using reitit.core/router, any exceptions thrown during the router creation process are caught, formatted, and rethrown. The formatting logic is determined by the exception formatter provided in the :exception router option. By default, reitit uses reitit.exception/exception, which produces single-color, partially human-readable error messages.
  8. Understand middleware execution order in reitit.ring

    master

    When using multiple middleware injection points, they execute in the following order (from outermost to innermost):

    1. Top-level middleware (passed as the second argument to ring-handler).
    2. Top-level route data middleware (defined in the :data key of the router).
    3. Parent route middleware (defined in the route data of a parent path).
    4. Route-specific middleware (defined in the route data of the matched path).
    5. The Handler.

    Example of execution order:

    (def app
      (ring/ring-handler
        (ring/router
          ["/api" {:middleware [[wrap :3-parent]]}
           ["/get" {:get handler
                    :middleware [[wrap :4-route]]}]]
          {:data {:middleware [[wrap :2-top-level-route-data]]}})
        nil
        {:middleware [[wrap :1-top]]}))
    
    ;; Resulting execution order for /api/get:
    ;; [:1-top :2-top-level-route-data :3-parent :4-route :handler]
    (def app
      (ring/ring-handler
        (ring/router
          ["/api" {:middleware [[wrap :3-parent]]}
           ["/get" {:get handler
                    :middleware [[wrap :4-route]]}]]
          {:data {:middleware [[wrap :2-top-level-route-data]]}})
        nil
        {:middleware [[wrap :1-top]]}))
  9. Implement request-method based routing

    master

    Reitit allows you to define handlers for specific HTTP methods. You can place handlers at the top level (applying to all methods for that path) or under specific method keys (:get, :head, :patch, :delete, :options, :post, :put, or :trace).

    • Top-level handler: Used if no method-specific handler is found.
    • Method-level handler: Only catches the specified method.
    • :options method: Enabled by default for all paths to support CORS.

    Example:

    (def app
      (ring/ring-handler
        (ring/router
          ["/all" handler] 
           ["/ping" {:name ::ping
                     :get handler
                     :post handler}]])))
    (def app
      (ring/ring-handler
        (ring/router
          ["/all" handler]
           ["/ping" {:name ::ping
                     :get handler
                     :post handler}]])))
  10. Handle name conflicts in reitit routers

    master

    Reitit prevents multiple routes from sharing the same name. If you define routes with duplicate names (e.g., using ::name syntax), reitit.core/router will throw a CompilerException with an ex-info containing the conflicting names.

    Note: Unlike path conflicts, there is currently no way to disable or override name conflict resolution.

    (require '[reitit.core :as r])
    
    (def routes
      [["/ping" ::ping]
       ["/admin" ::admin]
       ["/admin/ping" ::ping]])
    
    ;; This will throw: CompilerException clojure.lang.ExceptionInfo: Router contains conflicting route names:
    ;; :reitit.core/ping
    ;; -> /ping
    ;; -> /admin/ping
    (r/router routes)
  11. Use path parameters and catch-all parameters

    master

    Reitit supports two main styles for path parameters: colon syntax (:id) and bracket syntax ({id}).

    Path Parameters

    • Standard parameters: Use :name or {name}.
    • Qualified keyword parameters: Use {user/id} or {:user/id} to allow keys with slashes or colons in the resulting parameter map.
    • Catch-all parameters: Use *path or {*path} to match the remainder of the path.

    Syntax Rules

    • Colon syntax (:id): Parameters must end with a slash / or the end of the path string. They cannot be embedded in the middle of a string segment (e.g., file-:id.txt is not supported via colon syntax).
    • Bracket syntax ({id}): Parameters can start and stop anywhere. The character immediately following the closing bracket acts as the terminator.
    • Slash-free routing: Reitit supports paths without slashes, such as broker.{customer}.{device}.