Introduction

Choosing your first web framework is like choosing your starter pokemon. There isn’t a wrong choice but it will still affect a large portion of your following decisions. (Does it? I never played Pokemon…)

There is a pretty decent amount of options for web development in Clojure. One of the most common ones I’ve seen is Luminus for the backend and re-frame for the UI. This seems like a pretty decent choice and I even tried out Luminus for a bit. However, while going through the tutorial it felt like it was suited towards more traditional multi-page applications. That’s fine and all but I’d like to focus on the frontend and keep the backend simple.

I stumbled upon Fulcro. A very interesting but also less used Clojure framework. It started as an extension of the now-discontinued Om Next but is its own thing now. It tries to bridge the gap between front- and backend in the simplest way possible. Rich Hickey’s definition of simple that is. (you know what I’m talking about)

Why Fulcro?

Now, this should be the section that explains the differences of Fulcro compared to other frameworks like re-frame, Hoplon, Coast, etc. Sadly, I have no experience with pretty much any of them, so I can’t make a good comparison.

That being said, I think that Fulcro has many similarities with re-frame. They both store their application state in a global atom, keep the rendering mostly pure and create events as data structures before handing them over to an event system.

Fulcro extends this by keeping the events network agnostic, allowing you to just send it over the network as-is. Furthermore, it lets you easily define the remote connections and how they should be accessed(REST, GraphQL or Fulcro’s default EQL).

EQL is pretty much the Clojure alternative for GraphQL and is fully leveraged by Fulcro. By taking advantage of it you only really need to define a few functions to access and modify the data. How the data is stored or accessed is up to the developer. As a result, the example project only needs around 100 lines of server code to serve static pages with ring, access various people by their id, get a list containing either your friends or enemies and delete a person from a list.

This is pretty much the non-boilerplate code.

Accessing your data:

(ns app.resolvers
  (:require
   [com.wsscode.pathom.core :as p]
   [com.wsscode.pathom.connect :as pc]))

(def people-table
  (atom
   {1 {:person/id 1 :person/name "Sally" :person/age 32}
    2 {:person/id 2 :person/name "Joe" :person/age 22}
    3 {:person/id 3 :person/name "Fred" :person/age 11}
    4 {:person/id 4 :person/name "Bobby" :person/age 55}}))

(def list-table
  (atom
   {:friends {:list/id     :friends
              :list/label  "Friends"
              :list/people [1 2]}
    :enemies {:list/id     :enemies
              :list/label  "Enemies"
              :list/people [4 3]}}))

;; Given :person/id, this can generate the details of a person
(pc/defresolver person-resolver [env {:person/keys [id]}]
  {::pc/input #{:person/id}
   ::pc/output [:person/name :person/age]}
  (get @people-table id))

;; Given a :list/id, this can generate a list label and the people
;; in that list (but just with their IDs)
(pc/defresolver list-resolver [env {:list/keys [id]}]
  {::pc/input #{:list/id}
   ::pc/output [:list/label {:list/people [:person/id]}]}
  (when-let [list (get @list-table id)]
    (assoc list
           :list/people 
           (mapv (fn [id] {:person/id id}) (:list/people list)))))

;; Global Resolvers, takes no input
(pc/defresolver friends-resolver [env input]
  {::pc/output [{:friends [:list/id]}]}
  {:friends {:list/id :friends}})

(pc/defresolver enemies-resolver [env input]
  {::pc/output [{:enemies [:list/id]}]}
  {:enemies {:list/id :enemies}})

(def resolvers [person-resolver list-resolver
                friends-resolver enemies-resolver])

Modify your data:

(ns app.mutations
  (:require
   [app.resolvers :refer [list-table]]
   [com.wsscode.pathom.connect :as pc]
   [taoensso.timbre :as log]))

(pc/defmutation delete-person [env {list-id :list/id
                                    person-id :person/id}]
;; optional, this is how you override what symbol it responds to.
;; Defaults to current ns.
  {::pc/sym `delete-person}
  (log/info "Deleting person" person-id "from list" list-id)
  (swap! list-table update list-id update
    :list/people 
    (fn [old-list] (filterv #(not= person-id %) old-list))))

(def mutations [delete-person])

While I don’t know much about what’s going on in the background, I think it’s pretty easy to find out what you need to do in order to get what you want. All you need on the client is a mutation that mirrors the mutation on the server. The resolvers work as-is. (Obviously, you don’t have anything that uses your data yet)

Mutation on the client:

(ns app.mutations
  (:require
   [com.fulcrologic.fulcro.mutations :as m :refer [defmutation]]
   [com.fulcrologic.fulcro.algorithms.merge :as merge]))

(defmutation delete-person
  "Mutation: Delete the person with ':person/id' from the list with ':list/id'"
  [{list-id :list/id
    person-id :person/id}]
  (action [{:keys [state]}]
          (swap! state merge/remove-ident*
                 [:person/id person-id]
                 [:list/id list-id :list/people]))
  (remote [env] true))

The action is executed immediately on the client. The remote bit means that the exact same mutation with the same params should be executed on the server aswell.

Noteworthy additions

Fulcro also comes with a pretty large selection of helper functions for many aspects of web development, templates for both web apps and mobile apps, a complete book, a react component library based on Semantic UI, an inspection tool and much more.

Needless to say, Fulcro got me covered for pretty much any issue I might have in the future.