Web Dev Crash Course - Heroku, Clojure, Luminus
Table of Contents
⚠ Consider this article superceded by this newer article ⚠
This article teaches you how to make a traditional web application with Clojure and Luminus.
First we build a minimal app which reads and writes to a database. Then we move onto hosting it on Heroku, and finally move onto some middleware and user authentication.
Where you see:
- 🏃, there is an excercise for you to work out yourself.
- ✎, I show you the code changes I made when I did it.
- 🚩, this is something you should remember as you will need it again later
Note I am using Debian Bullseye as my development system. Also note I assume a working knowledge of Clojure in this article.
1. Quickstart; a minimal Luminus app
apt install clojure leiningen postgres
Template out a luminus project called "polls":
lein new luminus polls +immutant +postgres
Immutant is for the webserver and Postgres for the database.
cd polls # keep empty directories find . -depth -type d -empty -exec touch {}/.gitkeep \; # stop calva stuff ending up in git cat >> .gitignore <<EOF .calva/ .clj-kondo/ .lsp/ EOF git init git add . git commit -m "initial commit" lein repl
The repl runs in the user
namespace by default (see
env/dev/clj/user.clj
). This provides functions specifically to
help with dev, such as start
and stop
.
(start)
🚩 If you see errors in your REPL it can be because you forgot to
run (start)
. If you need to get back to your user namespace you
run (in-ns 'user)
.
Note you will want an extra terminal as you don't want to kill your repl every time you run a bash command.
When you ran start
it printed a warning database connection URL
was not found
. Fix it thus:
sudo -u postgres psql create database polls_dev; create user polls_dev with password 'EDITME'; grant all on database polls_dev to polls_dev;
Now update dev-config.edn
. (stop)
and (start)
the app. You
should no longer see the warning.
Now we are ready to start making our app. The first thing we will do is design our database tables.
Delete the example migrations in resources/migrations/
.
rm resources/migrations/*
Make new migrations:
(create-migration "question-and-choice")
🛈 if it errors you may need to run (start)
in your user namespace.
We now add SQL to the migrations to make (and unmake) our database tables.
In up:
create table question ( id serial not null primary key, question_text varchar(100), pub_date timestamp default current_timestamp ); --;; create table choice ( id serial not null primary key, question integer references question (id) on delete cascade, choice_text varchar(100), votes integer default 0 not null );
In down:
drop table choice; drop table question;
Run the migration:
(migrate)
🚩 If you run the (migrate)
and see error "Too many update results
were returned", you forgot to put --;;
between the queries.
Let's define a couple of queries. We do this by making a HugSQL SQL
file. Add the following to resources/sql/queries.sql
(you can
delete the example queries that are already there):
-- :name create-question! :! :n -- :doc creates a new question insert into question (question_text) values (:question_text) -- :name get-questions :? :* -- :doc retrieves all questions select * from question
This file will be parsed to create functions by the code in
src/clj/polls/db/core.clj
.
To make your repl pull in the changes without restarting it:
(in-ns 'polls.db.core) (conman/bind-connection *db* "sql/queries.sql")
🚩 remember to do this when you write more HugSQL queries in future
Now you can test out your new query functions:
(get-questions) (create-question! {:question_text "Would you love a monsterman?"}) (create-question! {:question_text "What time is it?"}) (get-questions)
Awesome! Now let's start writing our app.
The code for the routes is in src/clj/polls/routes/home.clj
.
The templates are in resources/html/
. These use Selmer.
🏃 Update the code and templates so that /
shows the list of
questions.
✎ changes
🏃 Add queries to create and list choices. Test them by adding some choices to your questions and listing them.
✎ changes
🏃 Make an extra page to show the choices for a question, based on a question id in a query string. Make the questions on the homepage link to it.
✎ changes
The next step is to allow to vote on the polls. This requires the use of a form and some extra queries and pages. This isn't easy to work out for yourself so for this bit I invite you to look at how I did it:
✎ changes
Regarding detail.html; note the {% csrf-field %}
. This adds a
hidden form input containing an anti-forgery token which Luminus
will validate.
Regarding home.clj; we are using Integer/parseInt
because we need
to convert the string we get from the HTTP query string into an
integer to pass to postgres.
2. Hosting it; 12-factor app Heroku
We want our app to be 12-factor so we can easily host it in modern hosting environments. A good way to prove 12-factor is to host it on Heroku.
Luminus makes this really easy as we already have a Procfile
and
.gitignore
. 🏃 Follow the guide here:
https://luminusweb.com/docs/deployment.html#heroku_deployment
A few things to note from when I did this:
- before I could run
heroku local
, I had to do the following:- add cheshire to the deps in project.clj
- run
lein uberjar
- make a file
.env
containing:DATABASE_URL=jdbc:postgresql://localhost/polls_dev?user=polls_dev&password=EDITME
heroku run lein run migrate
didn't work for me so I modified thestart-app
function.
✎ changes
3. Adding users and authentication
3.1. tables, queries and functions
We will start by adding the database tables and queries to create and authenticate users.
(create-migration "user")
In up:
create table users ( login text primary key, password text not null, created_at timestamp not null default now() );
In down:
drop table users;
Run the migration in the user namespace:
(migrate)
Then add code like this to add the authentication functions:
✎ changes
After restarting to pull in the deps you may test it:
(in-ns 'polls.auth) (create-user! "testuser" "testpass") (authenticate-user "testuser" "testpass") (authenticate-user "testuser" "wrongpass")
3.2. login page
Add code thus:
✎ changes
Note we made a seperate routes file because we will be applying a middleware to the main routes for authorization.
3.3. middleware
We can log in now but there's no reason to do so as the site works without login. So we need to add middleware to redirect to the login page if someone tries to access the polls without logging in.
Study the docs:
Now write your code thus:
✎ changes