Building a Clojure web app backed by Postgres
In this article, we’ll create a Clojure web app talking to a Postgres database. You should know, that I’m by no means an expert on the subject - this is just front-end developer exploring new areas. Big thanks to my friend Chris Wolfe who helped me understand a lot of concepts presented in this article (and was extremely patient at that).
The app
We’ll build a simple app for storing cooking recipes called NerdRecipes. I’ll continue working on the same concept app in future articles. You can find the final code for this part here: https://github.com/smogg/nerd-fitness. The app will be based on the simple Compojure template. The app will be talking to a Postgres database and it will have a few endpoints to save/fetch the recipes. It will be fully tested. In the future we will build authentication/authorization for our endpoints as well as the UI. The latter part most likely will be done in Reagent (with re-frame).
The beginning
Let’s create a new project by running lein new compojure nerd-recipes
. You should get a file structure similar to this:
The database
As mentioned earlier, we’ll use Postgres for our project. I won’t go into too much detail about setting up a database since a simple web search will reveal everything you need to know. I’m using Postgres app for Mac.
In Clojure, we’ll use jdbc.pool.c3p0
to set up a connection pool we’ll use for our app:
Update project.clj
with above dependencies:
:
Let’s setup a database connection, create
; src/nerd-recipes/db.clj
As you can see, we’ve set up our connection to look for a database named nerd_recipes
.
First, connect to the database from the command line using default Postgres user (or just double click postgres
in Postgresapp I mentioned before):
psql -U postgres
Next, run some SQL statements to create a new database and a new user for our app:
;;ALL PRIVILEGES ON DATABASE nerd_recipes TO admin;
Let’s save above commands under dev/init_dev_db.sql
for future use. You (or anyeone else) will be able to run the file using psql -U postgres -f dev/inti_dev_db.sql
from the command line.
Let’s try our database connection in Clojure REPL:
=>nil=>nil=>=>=>=>
If everything went well you should see similar responses. We required java.jdbc
and the connection pool we’ve set up previously in nerd-recipes.db
. Then we used java.jdbc
to create the test
table, add an entry in the same table and then we verified that everything actually got saved to the database. Last but not least, we removed the same table and left the database as we’ve found it.
The SQL (hugging)
Running jdbc/db-do-commands
with SQL strings in Clojure all the time is not pretty nor fun. Hugsql to the rescue - it will let you write plain SQL (with some additional helpers) and then automagically import those statements into your Clojure code (for example by defining functions in current namespace). It’s a great library with even better documentation that separates concerns you want separated.
Add [com.layerware/hugsql "0.4.8"]
in your project.clj
dependencies and create a new file under src/nerd_recipes/db/sql/recipes.sql
. Next, add a few simple SQL statements for creating the recipes
table, where we will store our app’s recipes. We are also creating a few handy statements we’ll use to create new or select recipes.
-- :name create-recipes-table :!NOT EXISTS recipes (id bigserial PRIMARY KEY,name varchar NOT NULL,description text NOT NULL,created_at timestamp NOT NULL default current_timestamp)-- :name drop-recipes-table :!IF EXISTS recipes-- :name insert-recipe :! :nINSERT INTO recipes (name, description)VALUES (:name, :description)-- :name recipe-by-name-like :? :1SELECT * FROM recipes WHERE name LIKE :name-like-- :name recipes-with-cols :? :*SELECT :i*:cols FROM recipes
Hugsql is gonna use the part after -- :name ...
to define Clojure functions under the corresponding name. The :?
or :!
part tells hugsql what kind of result is expected.
For recipes-with-cols
we used :*
on the :name
line, and :i:cols
later, in the sql statement. This is literally expanding a Clojure collection (items will be separated by commas) inside your SQL. Neat! Refer to Hugsql docs to learn more about what just happened.
Let’s now make those functions available in Clojure:
Create a new file under src/nerd_recipes/db/recipes.clj
; src/nerd_recipes/db/recipes.clj
The def-db-fns
macro is going to create functions in current namespace from SQL statements we defined before. The following functions should now be available:
nerd-recipes.db.recipes/create-recipes-tablenerd-recipes.db.recipes/drop-recipes-tablenerd-recipes.db.recipes/insert-recipenerd-recipes.db.recipes/recipe-by-name-likenerd-recipes.db.recipes/recipes-with-cols
Again, let’s make sure that everything is working as expected in the REPL:
=>nil=>=>1=>=>
Notice we no longer need java.jdbc
to run the commands. You can remove it from your project’s dependencies as we’ll no longer be using it in this project.
Let’s also create another namespace to separate database functions from the rest of our application’s code:
; src/nerd-recipes/recipes.clj
This way you won’t have to think about database logic at all when creating/getting recipes elsewhere in your app.
The migrations
Next, let’s set up database migrations. We’ll use Drift - a simple, file-based migration library for Clojure similar to Rails. Add [drift "1.5.2"]
in your :plugins
and :dependencies
sections inside project.clj
.
By default, Drift will call the config.migrate-config/migrate-config
function to get the settings map. Create that file and put some basic setup there:
; src/config/migrate_config.clj
Drift will look for migrations to run under the specified :directory
and will rely on two functions - current-db-version
and update-db-version
to get current database version and update it respectively. We’ll store that information inside schema_migrations
table, so let’s start by creating the corresponding .sql
and .clj
files (like we did in the previous step for recipes
table). Let’s put the file under src/config/db/sql/schema_migrations.sql
:
-- :name create-schema-migrations-table :!not exists schema_migrations (version integer not null,created_at timestamp not null default current_timestamp)-- :name current-db-version :? :1select * from schema_migrationsorder by created_at-- :name insert-db-version :! :ninsert into schema_migrations (version)values (:version)
src/config/db/schema_migrations.clj
We can now use the new namespace and write missing functions for config.migrate-config
:
; src/config/migrate_config.clj
You’ll notice we also added two new keys to Drift settings - :init
and :migration-number-generator
. First one makes sure that schema_migrations
table is present before we start migrating our database. The second one tells Drift which function to use when coming up with a new database version number. We used Drift’s built-in incremental-migration-number-generator
which basically bumps the current version number by one.
You should now be able to run lein migrate
to run the migrations. At the end of the process you should see a message similar to the following:
INFO: No changes were made to the database.
We don’t have any actual migrations written yet, so that’s expected. Let’s write one:
; src/migrations/001_create_tables.clj
Drift expects two functions to be present in a migration file - up
and down
. Running lein migrate
now should create the tables. The process should end with a message similar to the following:
INFO: Migrated to version: 1
You can double check that database migration was properly recorded in schema_migrations
table from the command line:
$ psql nerd_recipes -U admin -c "select * from schema_migrations"version | created_at---------+----------------------------1 | 2017-11-17 13:38:21.055254
The tests
Last but not least, let’s implement a few tests for our app. Let’s start by creating a new script that’s gonna create a database/user for testing (similar to what we did with development database):
dev/init_test_db.sql
;;ALL PRIVILEGES ON DATABASE nerd_recipes_test TO test_user;
and let’s run the script from the command line:
$ psql -U postgres -f dev/init_test_db.sql
Next, create a helper for our tests. It’s gonna run the init function we passed to Drift settings, run all the migrations for us as well as redef the connection to use the test database instead of the development one:
; test/nerd_recipes/test_utils.clj
Now we can test the recipes
namespace and verify our functions are working:
Run lein test
from the command line and make sure everything passes.
The summary
I hope it was fairly straight forward - please let me know if something was incorrect/unclear by emailing me at [email protected] To clean things up before the next article I’m gonna add the following plugins:
- environ - so we don’t have to hard-code database settings
- cljfmt - to clean up project’s indentation
- auto - to automatically re-run the tests
Next time we’ll actually create a few Compojure routes and add an authentication system.