Yipgo is a medium-sized project - it has about 9,500 lines of Clojure and Clojurescript including the tests. It’s not the first Clojure project I’ve started and yet I continue to learn things every day that I work on the codebase. I’ve made plenty of mistakes and there’s some early decisions I made that I think I nailed - be it through luck or judgement. Below I’ll outline some of those decisions and how they’ve affected the build, hopefully it’ll be useful to developers, especially Clojure ones.
For a quick bit of context Yipgo is a ticketing system, a little like a light-weight, fast Jira. It’s built with responsiveness and ease of use in mind. Ok, context and a bit of marketing. :)
Not using clojure.spec earlier
Clojure is a dynamically typed language. This means that type checking happens at runtime and removes the safety net that strongly typed languages benefit from.
There’s a relatively young library which is developed by the core Clojure team called Clojure.Spec. It’s job is to provide specifications for data and functions which can be checked when you run your tests and/or in development builds. If you spec your low-level, fundamental operations and datastructures then you will catch a plethora of issues that may have passed by until production runtime. It’s a great compromise when it comes to dynamic vs typed trade-offs. I’m sure other languages have similar systems but this one is ours.
Despite the sales pitch above, I avoided Spec. I would like to think it was out of concern for the evolving API and the - at the time - shoddy error messages, but in reality I expect it was lazyness.
Not surprisingly it was a huge mistake as I had some bugs that would have been caught early with the system in place. It was a particularly silly bug that ultimately persuaded me to start using Spec and over a couple of days I retrofitted the backend code with the checks and found an embarrassing amount of issues that I could fix without them biting me in production. Spec allowed me to solve a hefty chunk of technical debt in one fell swoop, if you’re a Clojure person, then please make it part of your toolkit.
Not making use of front/back shared structures
Clojure has a method for sharing code between the backend and the
frontend through the use of
.cljc files that can, in theory, be
compiled by both the Clojure and Clojurescript compilers. Though the
use of reader macros (think of them as language syntax for now), you
can run snippets of code conditionally for either platform. For
example, here’s a simple log function that can be imported into both
front and back-end code which will behave similarly:
(ns yipgo.c.log) (defn dbg [obj] ;; this line will be compiled by the Clojure compiler (#?(:clj clojure.pprint/pprint ;; this line will be compiled by the Clojurescript compiler :cljs cljs.pprint/pprint) obj) obj)
To a non-Clojure developer, apologies, it’s going to be a little hard
to parse the first time you see it. Basically, there’s a conditional
defined there using the macro
#? that tells either the Clojure
:clj) or the Clojurescript (
cljs) compilers which bits they
should compile and what to ignore. This means that I can use the
function all over the codebase. It can be a bit ugly - think
in C - but it’s so handy to maintain identical datastructures between
the back and front.
Early in the project I didn’t bother with the shared code and found
myself writing very similar back and frontend code, negating the
benefits of having one language for everything. More recently I’ve
been porting my models to
cljc files and therefore allowing them to
be spec’d during development as well as saving time and debugging
effort changing them in two places. The data I send to the REST API
from the front-end isn’t identical to the structure I eventually give
to the database, so there’s some munging/inheritance involved, but
it’s a small price to pay.
This one is a much more generic issue and I’m a bit ashamed to admit
it but the project has maintained about 80% back-end test coverage and
at best 10% front-end coverage. Clojurescript comes with
which is just like its
clj.test counterpart that I use extensively
for the backend so it’s not a particularly hard thing to get
going. Writing and especially refactoring code without tests is a bit
like riding a bike without a helmet.
However, code that is shared between the front and back only needs testing once, so that allows a little gap in the coverage.
Picking Clojure itself
This already feels like a bit of a fanboy homage to Clojure, so I’ll let others tell you why Clojure is a good pick:
EDN (extensible data notation [eed-n]) is another clojure-specific technology. A quick way to describe it is as the language’s equivalent of JSON. However, EDN has different goals over JSON which I think makes it a much better fit for what I’m doing here at Yipgo.
First and foremost, it’s extensible, this means you can build readers to handle custom elements within the EDN stream.
Secondly, it comes with a rich set of default elements including datetimes and uuids, both of which are used extensively in Yip.
The Yipgo rest API speaks in EDN to the backend so there are minimal conversions happening - I’ve not had to implement any custom readers yet. If I’d have done the data transfer in JSON, there would have to be logic to support uuids, dates etc.
The disadvantage with this is that browsing JSON datastructures in a
browsers console (after, say a
console.log) is a reasonably pleasant
experience as it’s formatted to an interactive, browsable tree. EDN
gets dumped out as a plain string. Small enough price to pay.