June 8, 2015

Throwing together a weekend project with boot-clj

Keeping up with the boot zeitgeist

This weekend I made http://worldclassifiedlist.com on a whim. Past experience suggests that I’m not likely to derive any value from the site itself, so before it’s completely forgotten, overlooked by an indifferent world and relegated to an unnoticed screenshot on my portfolio page, I thought I might wring a few ideas out of the making-of.

Boot pulls its weight

This is the first project I’ve done using boot from the get-go, which went swimmingly. After a bit of cleanup, my .boot file for the project sits at 82 lines, but those 82 lines are doing a fair bit of work, managing the environment, providing dev and production server configurations, and handling middleman and cljs compilation.

The project architecture is simple: The API server exposes a few thinly-wrapped postgres queries. Clojurescript powers the frontend using Reagent and Kioo, which talks to the API endpoints for data. The actual HTML and CSS are generated using Middleman, as is my habit, using the boot-middleman library.

All of that is wrapped up in one boot command:

(deftask dev
  "Start a dev server"
  []
  (comp
    (serve :reload true) ; Serves the API and static pages
    (repl)
    (watch)
    (middleman :dir "html")
    (cljs :optimizations :none :source-map true)))

While running this command, any changes to the clj, cljs or middleman files triggers a rebuild of the appropriate fileset, which ends up pretty close to live development. Unfortunately, the cljs repl and the cljs reload tasks didn’t play nice with serving from a ring handler, so I just left those out.

Migrations

I’ve gotten into the bad habit of using Mongo as my datastore in projects like this, but this time I decided to go with good old postgres. This means that I have to define a schema in advance, and manage the state of said schema.

The tool I chose for this purpose is boot-ragtime. Ragtime really does two things:

  1. Generates timestamped .up.sql and .down.sql files in the migrations directory for you, and 2) applies those migrations. It’s not as nice a system as, say, Rails’ migrations, nor South or Django migrations or even Alembic, since it can’t automatically generate anything for you. But it’s still pretty easy to use if you don’t mind writing a little SQL yourself, and Clojure mostly spares you from having to write full table/model definitions in exchange.

I tossed the ragtime options in a task-options! call in build.boot to save some trouble:

(task-options!
  ragtime {:driver-class "org.postgresql.Driver"
           :database (str "jdbc:postgresql://localhost:5432/"
                          (:database-name env)
                          "?user=" (:database-user env)
                          "&password=" (:database-password env))})

With those settings in place, it becomes very easy to use: boot ragtime -g "My New Migration" to generate a new migration with a given name, and boot ragtime -m to apply all future migrations.

Environ

As you can see from the above, I decided to do configuration the proper way, using environment variables assisted by environ. I initially attempted to use boot-environ to manage my dev settings, but I ran into issues where I needed environment variables to set up database connections and the http port in task-options!, which runs before boot-environ gets a change to do its thing. A quick read of boot-environ’s source led me to a quick solution: mangle the env var in-place with the dev server settings as defaults. Here’s how that looks:

(require '[environ.core :refer [env]])

; Mangle environ's env var to provide some defaults
(def env-defaults {
    :database-name "classifieds"
    :database-user "classifieds_user"
    :database-password "QmXYGLaN3vhxK3hyaMzRhV9SWFRWGxkd"
    :port "8080"})

(alter-var-root #'env #(merge env-defaults %))

Now, the environment variables are present for the rest of the app’s lifecycle. Huzzah for mutating global variables!

If you’re curious about the rest of the project, the whole hot mess is available for your perusal on Github.