Liking cljdoc? Tell your friends :D

Endpoint Routes and Handlers

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.

Basic Routes and Handlers

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:

route example
["/users/{id}" {:name    :users
                :handler (fn [req] {:body "response"})}]

Namespace Routes

It can get tedious to write a bunch of routes that look something like this:

tedious routes
[["/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:

basic namespace route
(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.

Handlers

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:

To-Do app’s 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):

decisions
(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:

handlers for the impatient
(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:

handlerCRUD operation

[:collection :get]

READs a collection of entities

[:collection :post]

CREATEs an entity

[:member :get]

READs a single entity using an identifier

[:member :put]

UPDATEs an entity

[:member :delete]

DELETEs an entity

Summary

  • 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.

Cross Compilation

So far this doc has focused on how routes are used to convey requests to handlers. Routes can also be used to generate paths:

decisions
(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!

What’s Next?

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