["/users/{id}" {:name :users
:handler (fn [req] {:body "response"})}]
These docs explain how to write endpoint routes and handlers in a Sweet Tooth app. If you’re unfamiliar with routes or handlers, check out the Introduction to Request Handling.
This page covers practical basics, just enough for you to get some stuff working. For more practical, bottom-up instruction, Routes in Depth covers every facet of writing routes, and Handlers in Depth explains how to write Liberator handlers. Responses covers Sweet Tooth’s response protocol.
Server Architecture: Components and Beyond explains how these sub-systems fit into the larger whole. It describes how they’re composed using the Duct and Integrant architecture composition micro-frameworks. It’s light on HOWTO instructions, but the high level perspective it provides should help you understand routes and handlers better.
These docs assume you’re working within the the Sweet Tooth To-Do List Example project.
Sweet Tooth introduces conventions for implicitly associating route paths, route names, and the namespaces that hold handler definitions. This eliminates boilerplate but might cause some to shake their heads in disgust with a frowning, condemnatory grumble of "Magic!" These docs explain how everything works, hopefully to the satisfaction of those grumpy individuals who have somehow lost their love of freakin' magic.
Sweet Tooth is oriented around RESTful APIs. You send GET
, POST
, PUT
, and
DELETE
requests to paths like /todo-list
and /todo-list/1
in order to
perform CRUD operations. The corresponding request handlers are located in a
namespace like sweet-tooth.todo-example.backend.endpoint.todo-list
.
For routing, Sweet Tooth relies on the reitit library, which represents routes
as two-element vectors that associate URL patterns like /users/{id}
with a map
containing the route’s name, handler, and metadata. For example:
["/users/{id}" {:name :users
:handler (fn [req] {:body "response"})}]
It can get tedious to write a bunch of routes that look something like this:
[["/user" {:name :users
:handler project.endpoint.user/list}]
["/user{id}" {:name :user
:handler project.endpoint.user/show}]
["/todo-list" {:name :todo-lists
:handler project.endpoint.todo-list/list}]
["/todo-list/{id}" {:name :todo-list
:handler project.endpoint.todo-list/show}]]
To reduce this tedium, the
sweet-tooth.todo-example.cross.endpoint-routes/expand-routes
(which I’ll also
refer to as serr/expand-routes
) function lets you specify the names of
namespaces that contain handlers and uses those to generate routes. Let’s
generate some simple routes in a REPL and work our way up to more complex ones:
(require '[sweet-tooth.endpoint.routes.reitit :as serr])
(serr/expand-routes
[[:sweet-tooth.todo-example.backend.endpoint.todo-list]])
;; =>
[["/todo-list"
{::serr/ns :sweet-tooth.todo-example.backend.endpoint.todo-list
::serr/type :collection
:name :todo-lists}]
["/todo-list/{id}"
{::serr/ns :sweet-tooth.todo-example.backend.endpoint.todo-list
::serr/type :member
:name :todo-list
:id-key :id}]]
The function took a single keyword corresponding to a namespace’s name and
generated two routes for it, one with the path "/todo-list"
named
:todo-lists
and one with the path "/todo-list/{id}" `named `:todo-list
.
These paths and names are derived from the namespace name, with endpoint.
as
the default delimiter.
Routes are supposed to convey a request to a handler, and with reitit routes you
designate a handler with the :handler
key. The :handler
key is conspicuously
missing from the above routes. So how does this work?
The Sweet Tooth module :sweet-tooth.endpoint.module/liberator-reitit-router
adds the :handler
key to routes. You can see see an example of the module
being used in the To-Do app’s resources/config.edn
file:
:sweet-tooth.endpoint.module/liberator-reitit-router
{:routes sweet-tooth.todo-example.cross.endpoint-routes/routes}
The module references sweet-tooth.todo-example.cross.endpoint-routes/routes
,
which contains a vector of routes as returned by serr/expand-routes
. It
modifies these routes, adding a :handler
key to each. It uses the metadata
keys ::serr/ns
and ::serr/type
to look up a liberator decision map and
construct a liberator handler. The updated routes are passed to
reitit.ring/router
to create a ring-compatible request handler.
::serr/ns
is used to find a liberator decision map. By default, these are
defined in a var named decisions
. If you look at
sweet-tooth.todo-example.backend.endpoint.todo-list
namespace you’ll see
something like this (I’ve elided irrelevant code):
(def decisions
{:collection
{:get {:handle-ok (comp tl/todo-lists ed/db)}
:post {:post! ed/create->:result
:handle-created ed/created-pull}}
:member
{:get {:handle-ok (fn [ctx])}
:put {:put! ed/update->:result
:handle-ok ed/updated-pull}
:delete {:delete! (fn [ctx])
:handle-ok []}}})
decisions
is a map whose keys correspond to ::serr/type
in the routes above:
if a request for "/todo-list"
is received, the ::serr/type
value of
:collection
is used to look up the map of handlers under :collection
in the
decisions
var. The request method (:get
, :post
, :put
etc) is then used
to look up the decision map for that method. The decision map is passed to a
liberator function that returns a request handler.
If you’re unfamiliar with liberator this probably looks weird as all get out. I explain liberator fully in Handlers in Depth; for now we’re just focusing on the relationship between routes and handlers. If you’re wanting to just get stuff working, follow these rules:
Place your handlers under the :handle-ok
key, except for :post
requests.
For :post
requests, use the :handle-created
key.
Handler functions take one argument, which you should name ctx
. The ring
request is available under the :request
key of ctx
.
When returning entity data, the handler function should return a map or vector of maps for your entities.
In following these rules you’ll write code that looks like this:
(def decisions
{:collection
{:get {:handle-ok
(fn [ctx]
;; this is a constant, but you would probably have a function that
;; returns a sequence of records from a db
[{:id 1, :todo-list/title "to-do list"}])}
:post {:handle-created
(fn [{{:keys [params]} :request}]
(db/insert! :todo-list params))}}
:member
{:get {:handle-ok
(fn [ctx]
{:id 1, :todo-list/title "to-do list"})}
:put {:handle-ok
(fn [{{:keys [params]} :request}]
(db/update! :todo-list params))}
:delete {:handle-ok
(fn [{{:keys [params]} :request}]
(db/delete! :todo-list (:id params)))}}})
This outline corresponds to CRUD operations:
handler | CRUD operation |
---|---|
| READs a collection of entities |
| CREATEs an entity |
| READs a single entity using an identifier |
| UPDATEs an entity |
| DELETEs an entity |
There is a relationship between route paths, route names, namespaces, and handlers
Handlers are constructed from liberator decision maps
Those decision maps live in a var named decisions
decisions
is a map keyed by route type (:collection
, :member
) and
request method (:get
, :post
etc)
You can generate routes for an endpoint namespace using
sweet-tooth.endpoint.routes.reitit/expand-routes
. Route paths and names are
derived from namespace names.
So far this doc has focused on how routes are used to convey requests to handlers. Routes can also be used to generate paths:
(require '[reitit.core :as r])
(-> (serr/expand-routes [[:project.endpoint.todo-list]])
r/router
(r/match-by-name :todo-list {:id 1})
:path)
;; =>
"/todo-list/1"
The frontend makes ample use of this capability to generate URLs for API calls.
Thankfully we can define our routes in one endpoint_routes.cljc
file and it
will get cross-compiled to both frontend and backend targets. Pretty sweet!
Routes in Depth covers every facet of writing routes, and Handlers in Depth explains how to write Liberator handlers. Responses discusses Sweet Tooth’s response protocol.
Server Architecture: Components and Beyond explains how these sub-systems are composed using the Duct and Integrant architecture composition micro-frameworks.
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close