Liking cljdoc? Tell your friends :D

Routes In Depth

Route Expanders

What if you only want to generate a :collection route or only want to generate a :member route? Routes can take an option map, and you can specify which routes to generate with the key ::serr/expand-with:

route generators
(serr/expand-routes
 [[:project.endpoint.todo-list {::serr/expand-with [:collection]}]])
;; =>
[["/todo-list"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]]

(I’ve switched from :sweet-tooth.todo-example.backend.endpoint.todo-list to :project.endpoint.todo-list because the latter is much shorter, and to show reinforce that everything up to endpoint. is ignored when generating paths and route names.)

Notice that the value of for ::serr/expand-with is [:collection] and only a :collection route was generated. You can try this with [:member] to see what happens. The default value for ::serr/expand-with is [:collection :member].

In this context, :collection and :member are names of route types. Each route type has an expansion strategy associated with. The expansion strategy includes:

  • A rule for deriving the route’s name from the namespace’s name. The :collection strategy produces a route named :todo-lists when given a namespace name :x.endpoint.todo-list; :member produces a route named :todo-list.

  • A rule for deriving the route’s path from the namespace’s name. The :collection strategy generates the path /todo-list and :member generates /todo-list/{id}.

In later sections you’ll see how to work with additional kinds of route types, include :singleton, :member children, and arbitrary types.

Custom Route Paths and Names

What if you want to create routes that match paths like the following?

  • /api/v1/todo-list

  • /todo-lists

  • /todo-list/{id}/todo-items

  • /admin/todo-list

Custom Route Paths: prefixes and suffixes

You can specify paths with the keys ::serr/path-prefix and :serr/path-suffix:

path prefixes
(serr/expand-routes
 [[:project.endpoint.todo-list {::serr/path-prefix "/api/v1"}]])
;; =>
[["/api/v1/todo-list"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]
 ["/api/v1/todo-list/{id}"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :member
   :name       :todo-list
   :id-key     :id}]]

Custom Route Paths per route type

::serr/path-prefix was applied to both of the generated routes, but what if you need to modify the path for just one route type?

custom paths per route type
(serr/expand-routes
 [[:project.endpoint.todo-list {::serr/expand-with [[:collection {::serr/path-prefix "/api/v1"}]
                                                    :member]}]])
;; =>
[["/api/v1/todo-list"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]
 ["/todo-list/{id}"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :member
   :name       :todo-list
   :id-key     :id}]]

You can specify options for each route type under ::serr/expand-with by adding a pair, [:route-type options-map].

::serr/path lets you specify a replacement for just the part of the path that’s generated by the route type. Here’s how you could generate /todo-lists and /api/v1/todo-lists:

per-route-type paths
(serr/expand-routes
 [[:project.endpoint.todo-list {::serr/expand-with [[:collection {::serr/path "/todos"}]]}]])
;; =>
[["/todos"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]]

(serr/expand-routes
 [[:project.endpoint.todo-list {::serr/expand-with [[:collection {::serr/path-prefix "/api/v1"
                                                                  ::serr/path "/todos"}]]}]])
;; =>
[["/api/v1/todos"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]]

You might be wondering why you would specify both ::serr/path-prefix and ::serr/path. In the above case it doesn’t necessarily makes sense. It makes more sense when you consider that route options can be applied to multiple routes. We saw that above when ::serr/path-prefix was applied to both :member and :collection routes. In a later section you’ll see how to specify route options for groups of namespace routes.

Member Routes

What if you wanted to route a path like "/todo-list/{id}/todo-items"?

member routes
(serr/expand-routes
 [[:project.endpoint.todo-list {::serr/expand-with [[:member/todo-items]]}]])
;; =>
[["/todo-list/{id}/todo-items"
  {::serr/ns   :project.endpoint.todo-list,
   ::serr/type :member/todo-items,
   :name       :todo-list/todo-items,
   :id-key     :id}]]

You add a route type of :member/todo-items. It generates a route with the desired path and the name :todo-list/todo-items. In the corresponding namespace, you would define handlers with something like:

member route handlers
(def decisions
  {:member/todo-items
   {:get {:handle-ok (fn [ctx])}
    :post {:handle-created (fn [ctx])}}})

Remember, the keys in decisions correspond to route types, and you generated the route above with the type :member/todo-items.

Nested Routes

How about routing "/admin/todo-list" and "/admin/todo-list/{id}"? You could use ::serr/path-prefix, but you probably also want the handlers to live in a separate namespace and to use separate route names. Here’s how you’d do it:

nested routes
(serr/expand-routes
 [[:project.endpoint.admin.todo-list]])
;; =>
[["/admin/todo-list"
  {::serr/ns   :project.endpoint.admin.todo-list
   ::serr/type :collection
   :name       :admin.todo-lists}]
 ["/admin/todo-list/{id}"
  {::serr/ns   :project.endpoint.admin.todo-list
   ::serr/type :member
   :name       :admin.todo-list
   :id-key     :id}]]

Arbitrary Routes

The expand-routes function only performs route expansion when it encounters vectors where the first element is a keyword, like [:project.endpoint.admin.todo-list]. In addition to these namespace-based routes, you can also write plain ol' reitit routes. The next example matches a regular reitit route with a namespace route:

arbitrary routes
(serr/expand-routes
 [["/init" {:name :init}]
  [:project.endpoint.todo-list]])
;; =>
[["/init" {:name :init}]
 ["/todo-list"
  {::serr/ns   :project.endpoint.todo-list,
   ::serr/type :collection,
   :name       :todo-lists}]
 ["/todo-list/{id}"
  {::serr/ns   :project.endpoint.todo-list,
   ::serr/type :member,
   :name       :todo-list,
   :id-key     :id}]]

The regular route isn’t touched. One non-obvious consequence of this is that you’ll need to supply a :handler key yourself; Sweet Tooth uses the ::serr/ns and ::serr/type keys to construct a handler, but those are absent. You can add a handler as an integrant ref or by using the sweet-tooth.endpoint.utils/clj-kvar function:

handlers for arbitrary routes
(serr/expand-routes
 [["/init" {:name :init
            :handler (ig/ref :project.endpoint.init/handler)}]])

(serr/expand-routes
 [["/init" {:name    :init
            :handler (sweet-tooth.endpoint.utils/clj-kvar :project.endpoint.init/handler)}]])

The clj-kvar function returns the corresponding var during Clojure compilation and returns the keyword during ClojureScript compilation. This makes it easier to write routes that can cross-compile.

You should use an integrant ref if the handler needs to participate in integrant’s configuration system - if you need to initialize the handler with environment variables or system components, for example. Using clj-kvar would let you forego integrant initialization and keep your integrant config a little leaner.

Shared Route Options

What if you want to give multiple routes a prefix or otherwise want to apply options to multiple routes?

shared route options
(serr/expand-routes
 [{::serr/path-prefix "/api/v1"}
  [:project.endpoint.todo-list]
  [:project.endpoint.todo]])
;; =>
[["/api/v1/todo-list"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]
 ["/api/v1/todo-list/{id}"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :member
   :name       :todo-list
   :id-key     :id}]
 ["/api/v1/todo"
  {::serr/ns   :project.endpoint.todo
   ::serr/type :collection
   :name       :todos}]
 ["/api/v1/todo/{id}"
  {::serr/ns   :project.endpoint.todo
   ::serr/type :member
   :name       :todo
   :id-key     :id}]]

expand-routes takes a vector as its argument. Whenever it encounters a vector in that map, as it does with {::serr/path-prefix}, it adds that map as route options for all the routes that follow. If one group of routes need a set of common options that differs from another group of routes, you could write something like this:

multiple sets of shared route options
(serr/expand-routes
 [{::serr/path-prefix "/api/v1"}
  [:project.endpoint.todo-list]

  {:id-key :db/id}
  [:project.endpoint.todo]])
;; =>
[["/api/v1/todo-list"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :collection
   :name       :todo-lists}]
 ["/api/v1/todo-list/{id}"
  {::serr/ns   :project.endpoint.todo-list
   ::serr/type :member
   :name       :todo-list
   :id-key     :id}]
 ["/todo"
  {::serr/ns   :project.endpoint.todo
   ::serr/type :collection
   :name       :todos
   :id-key     :db/id}]
 ["/todo/{db/id}"
  {::serr/ns   :project.endpoint.todo
   ::serr/type :member
   :name       :todo
   :id-key     :db/id}]]

Notice that todo routes have a different :id-key and they also don’t have the /api/v1 prefix. Whenever a new common options map ({:id-key :db/id}) is encountered, it replaces the previous map ({::serr/path-prefix "/api/v1"}).

Misc. Notes

Reitit lets you to express path prefixes with data structures like

reitit nested routes
["/api"
 ["/todo-list" {:name :todo-lists}]
 ["/todo"      {:name :todos}]]

Personally, I have an aversion to using nested data structures to represent nested resources. I’ve found that it becomes a lot easier to get lost in navigating the data structures, and it can get difficult to determine what values might be cascading through the nested layers, or what the relationships among the layers might be. Ultimately what we’re producing is a lookup table, and I personally find it much easier to reason about such a table if there isn’t any nesting.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close