Realworld Clojure in a Vase on a Pedestal
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": "jake@jake.jake",
"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":"thos37@gmail.com","user/bio":"coding the thosmos"}]}' http://localhost:8080/api/realworld-vase/v1/user
To be continued ...