May 6, 2018

Realworld Clojure in a Vase on a Pedestal

image 1

In this exceedingly complex "modern" and ever increasingly fast paced world of internet technological development, it's amazing to see an effort like the RealWorld Example Apps, "an Exemplary fullstack Medium.com clone," offer a path of unity and coherence in the midst of the chaos of so many options. It's a thorough front- and back-end API spec for a non-trivial demo app that many different people have built both front- and back-ends for, including re-frame, Keechma, React, Elm, Vue, Scala, Rails, Node, Django, Go, and many more.

Currently there are a couple of Clojurescript frontends, but no complete Clojure backend. I've never built anything with Paul de Grandis' Vase, but I've admired its data-driven architecture for declaratively building APIs fast. I'm curious how well it would do at building a Real World API backend. There's only one way to find out ...

The First step is to create a new Vase app:

lein new vase realworld-vase
 

This creates a basic ready-to-run Vase app:

realworld-vase
├── Capstanfile
├── Dockerfile
├── README.md
├── boot.properties
├── build.boot
├── config
│   └── logback.xml
├── project.clj
├── resources
│   └── realworld-vase_service.edn
├── src
│   └── realworld_vase
│       ├── server.clj
│       └── service.clj
└── test
    └── realworld_vase
        ├── service_test.clj
        └── test_helper.clj

6 directories, 12 files
 

Most of the magic happens in the EDN config file: resources/realworld-vase_service.edn

By default, the app has a few example API endpoints which we can see after running the app:

lein run

If we now browse to http://localhost:8080/api/realworld-vase/v1/db we'll see a list of all of the installed attributes in the sample Datomic database.

Our goal is to define a set of attributes to cover all of the necessary fields in the RealWorld API description.

The first entity type is the User:

{
  "user": {
    "email": "[email protected]",
    "token": "jwt.token.here",
    "username": "jake",
    "bio": "I work at statefarm",
    "image": null
  }
}
 
We can define this in the resources/realworld-vase_service.edn file by replacing the example user schema with the following:
:realworld-vase/user-schema
{:vase.norm/requires [:realworld-vase/base-schema]
 :vase.norm/txes [#vase/schema-tx
   [[:user/username :one :string :identity "A user's unique identifier"]
    [:user/email :one :string :unique "A user's email"]
    [:user/bio :one :string :fulltext "A short blurb about a user"]
    [:user/company :one :ref "A user's employer"]
    [:user/image :one :uri "URL of a user's photo"]
    [:user/token :one :string :index "A user's auth token"]]]}
 

Vase idempotently applies DB schema using conformity, so we'll need to restart the server in order to see our new attributes.

lein run
 
Now we should see our new attributes in the db endpoint http://localhost:8080/api/realworld-vase/v1/db. (Search for user/username and user/email for example.)

Further down in our realworld-vase_service.edn file, we can change the definition for the /users endpoint to the following:

 "/users/:username" {:get #vase/query
    {:name :realworld-vase.v1/user-id-page
     :params [username]
     :edn-coerce [username]
     :query [:find ?e
             :in $ ?username
             :where
             [?e :user/username ?username]]}}

 "/user" {:get #vase/query {:name :realworld-vase.v1/user-page
                            ;; All params are required to perform the query
                            :params [email]
                            :query [:find ?e
                                    :in $ ?email
                                    :where
                                    [?e :user/email ?email]]}
          :post #vase/transact {:name :realworld-vase.v1/user-create
                                ;; `:properties` are pulled from the parameters
                                :properties [:db/id
                                             :user/username
                                             :user/email
                                             :user/bio]}
          :delete #vase/transact {:name :realworld-vase.v1/user-delete
                                  :db-op :vase/retract-entity
                                  ;; :vase/retract-entity requires :db/id to be supplied
                                  :properties [:db/id]}}
 
We should now be able to transact and query some users via the API.
curl -H "Accept: application/json" -X POST -d '{"payload": [{"user/username":"thosmos","user/email":"[email protected]","user/bio":"coding the thosmos"}]}' http://localhost:8080/api/realworld-vase/v1/user
 

To be continued ...

Tags: clojure programming