(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}]]
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
:
(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.
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
You can specify paths with the keys ::serr/path-prefix
and
:serr/path-suffix
:
(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}]]
::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?
(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
:
(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.
What if you wanted to route a path like "/todo-list/{id}/todo-items"
?
(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:
(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
.
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:
(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}]]
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:
(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:
(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.
What if you want to give multiple routes a prefix or otherwise want to apply options to multiple routes?
(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:
(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"}
).
Reitit lets you to express path prefixes with data structures like
["/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