PurelyFunctional.tv Newsletter 427: CRUD Handlers

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

Issue 427 - May 17, 2021 · Archives · Subscribe

Clojure Tip 💡

CRUD Handlers

One underappreciated feature of Ring is how little it does. There's a list of standard web tasks that it simply punts on. Here's a partial list:

  • Parsing query strings
  • Authorization
  • Authentication
  • Routing
  • Security
  • Cookies
  • Sessions

In most web frameworks, these would be considered table stakes. Ring takes an entirely different approach. It asks: What is the smallest abstraction with the most leverage---without limiting power? Then it gives you a way to add all of the other functionality.

The items in that list are not part of the Ring spec, but Ring as a library does provide some tools for performing those tasks. Those tools fit into the Ring ecosystem because they are either handlers or middleware. Using them is left as a choice for you, the programmer. There is a lot of power in keeping them separate---a small core with Turing-complete extension points.

We do something similar with our CRUD system. We kick the can down the road by assuming we can add them in later through composition. Our system will look a lot like Ring: Adapters, Handlers, and Middleware.

Adapters and Handlers

Adapters turn something into a CRUD request (which we defined in the last issue) and pass it to a handler. The handler returns the CRUD response (also defined in the last issue), which the adapter turns into whatever is appropriate.

We can imagine an adapter that turns Ring requests into CRUD requests and CRUD responses into HTTP responses. The adapter would itself be a Ring handler. Here's what it might look like, using Compojure to parse the path:

(defn crud-adapter [crud-handler]
  (routes ;; returns a Ring handler
   (POST "/api/v1/:entity" [entity :as req]
         (crud-handler {:operation :create
                        :entity-type entity
                        :entity (:body req)})) ;; assume body parsed as json
   (GET "/api/v1/:entity" [entity]
        (crud-handler {:operation :list
                       :entity-type entity}))
   (GET "/api/v1/:entity/:id" [entity id]
        (crud-handler {:operation :read
                       :entity-type entity
                       :id id}))
   (PUT "/api/v1/:entity/:id" [entity id]
        (crud-handler {:operation :replace
                       :entity-type entity
                       :id id
                       :entity (:body req)}))
   (PATCH "/api/v1/:entity/:id" [entity id]
          (crud-handler {:operation :merge
                         :entity-type entity
                         :id id
                         :entity (:body req)}))
   (DELETE "/api/v1/:entity/:id" [entity id]
           (crud-handler {:operation :delete
                          :entity-type entity
                          :id id}))))

This probably isn't perfect but it's a good first approximation. It converts the standard HTTP verbs and path patterns into the corresponding CRUD operations. It calls the handler with that operation.

Because the handler is just a function, we can implement it in a number of ways. For instance, we could have a handler for each entity type and dispatch to it:

(def entity-dispatch {"user" user-handler
                      "document" document-handler
                      ...})

(defn dispatch-handler [crud-request]
  (if-some [handler (get entity-dispatch (:entity crud-request))]
    (handler crud-request)
    {:status :entity-type-not-found
     :entity-type (:entity crud-request)
     :message (str "The "" (:entity crud-request) "" entity type is
unknown.")}))

This would let us store each entity in a different database.

Or we could dispatch first on the CRUD operation, like this:

(def operation-dispatch {:create create-handler
                         :delete delete-handler
                         ...})

(defn dispatch-handler [crud-request]
  (if-some [handler (get operation-dispatch (:operation crud-request))]
    (handler crud-request)
    {:status :operation-not-allowed
     :operation (:operation crud-request)
     :message "The "" (:operation crud-request) "" operation is not
allowed."}))

Doing it this way means we might store all of the entities in the same way in the database.

The choice is up to us. And it should be because each implementation will have different requirements.

There are still a lot of features we'd want to add. Some of those features are cross-cutting concerns that we implement in an orthogonal way. We'd use Middleware for that. We'll get to that next time.

Podcast episode🎙

This week on the podcast, I explore the functional mindset and whether "only pure functions is" is a good way to think about it . What can Haskell's do notation tell us about functional thinking?

Book update ✍️

Sales of Grokking Simplicity are doing okay. I'm not going to retire on the income. But it's a good sign that people are finding the book useful. Part of my mission with the book was to start a discussion in the FP literature geared toward commercial software, not academia. We need more books that are practical and guiding for professional programmers.

If you want a copy, you can buy one here. If you use the coupon code TSSIMPLICITY, you will get 50% off, which will more than offset the cost of shipping.

But, if you really like Amazon, please pre-order your copy now. It will be shipped starting May 18 (tomorrow as I write this!). However, it would really help me as an author. Amazon counts all pre-orders as sales on the day of release. And Amazon's algorithm rewards big sales days. So, I'm calling in a favor: If you were going to get it on Amazon anyway, please order it now rather than wait.

Shortly after it launches on Amazon, I will call in another favor: If you like it, a 5-star review!

Quarantine update 😷

I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.

I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.

Stay healthy. Wash your hands. Wear a mask. Take care of loved ones.

Clojure Challenge 🤔

Last issue's challenge

Issue 426

This week's challenge

Friday the 13ths

For some reason, when the 13th day of the month is a Friday, we consider that unlucky. Because it's such a popular superstition (and even those who don't believe in it enjoy the fun), we're making a website for it. Our job is to write some backed routines that the frontend team will add to pages.

Write the following:

  • A function that tells you the next upcoming Friday the 13th.
  • A function that tells you all the Friday the 13ths in a given year.
  • A predicate that tells you if a given year + month had a Friday the 13th.
  • A predicate to know if a given date (year, month, day) is a Friday the 13th.
  • A function that when the next upcoming Friday the 13th falls in a given month.

If you're working in JVM Clojure, use the Java 8 java.time package.

Thanks to this site for the problem idea, where it is rated Hard in Ruby. The problem has been modified.

Please submit your solutions as comments on this gist.

Rock on!
Eric Normand