Two majestic monoliths being majestic together and my car.
I like clojure. I also like noir, luminus create-react-app and rails. The clojure community has made a ton of great libraries that are all very simple. Unfortunately when I started putting them together, it took me a lot of time to understand the different decisions each library author made and how they worked together to get to a place where I had a working scaffold for my own bespoke full stack framework. I had some success doing this, but it still felt like I was playing catch up to all the great stuff other programming communities had. Not to mention every time I needed something else, like server reloading on each request, or refreshing code from the repl, it was another dependency and another namespace and another set of docs to read. So after slapping dependencies together in my project.clj
for the nth time, I made a clojure library and a leiningen template to help me launch projects faster. I should warn you, this isn’t the first time I’ve tried this but it is the most comprehensive one, and maybe I can convince some great clojure devs to help me out.
(:require [coast.core :as coast])
You can try it without the lein template if you want, just do this from your terminal:
mkdir simplecd simpletouch deps.ednecho '{:deps {coast {:mvn/version "0.6.1"}}}' >> deps.ednmkdir srctouch src/simple.clj
Then fill in simple.clj with the code below
It’s just clojure functions and maps built on top of existing libraries! That’s right, coast is not only a clever homage to rails, it’s also me literally coasting on the hard work of others! There’s a whole long list of libraries made and maintained by super talented developers that coast uses and gives credit to in the README.
Run the above code with clj -m simple
and now you’re ready for WORLD DOMINATION. Compete with google time? ✅ ✅
What happens if you start a fresh lein new coast your-proj
project? Now this is where the stack gets really full.
├── Procfile├── README.md├── profiles.clj├── project.clj├── resources│ └── public│ ├── css│ │ └── app.css│ └── js│ └── app.js├── src│ └── your_proj│ ├── components.clj│ ├── controllers│ │ ├── errors_controller.clj│ │ └── home_controller.clj│ ├── core.clj│ ├── routes.clj│ └── views│ ├── errors.clj│ └── home.clj├── target│ └── classes└── test└── your_proj└── core_test.clj
The default template is chock full of good stuff. I’m going to start with the aliases which should probably live somewhere else but for now they’re in your project. Deal with it 🕶.
lein db/createlein db/droplein db/migrationlein db/migratelein db/rollback
You can probably guess what’s going to happen when you navigate to your newly created project directory and type these things in. lein db/create
is going to create a new postgres database with a name like your_proj_dev
. lein db/drop
will drop this database. lein db/migrate
runs all migrations that haven’t run yet, lein db/rollback
rolls a migration back one at a time. lein db/migration create-posts
will create a new migration file in resources/migrations/{timestamp}_create_posts.edn
like this:
-- upcreate table posts (id serial primary key, title varchar(255) not null, body text not null, created_at timestamp without time zone default (now() at time zone 'utc')
);
-- downdrop table posts;
So the goal is to follow rails conventions, plural names for tables, id column for primary key, created_at timestamp.
So far things are pretty straightforward, and that’s boring so this is where things get kind of weird and start to diverge from rails quite a bit. There is no ORM here, only a VRM, so the models happen in two parts across two files.
There are no objects, no methods, and no dynamic sql generation (kind of), only static. Which means you have to write sql yourself. OH NO! But it’s ok, coast has your back.
lein sql/gen posts
This command takes the name of a table and generates sql in resources/sql/posts.sql
. Which looks like this
_-- name: all_select *from postsorder by created_at **desc
**_-- name: find-by-id-- fn: first_select *from postswhere id = :id
-- name: where-- fn: firstwhere id = :idreturning *
This SQL is just sitting here, doing nothing. Now it’s time to generate the “model” which is just a file with regular clojure functions.
lein model/gen posts
Which will create a new file: src/my_proj/models/posts.clj
:
Yay functions for the database!
That’s pretty much all there is to the models, I would like to add some spec-driven validation, but I haven’t done that yet, so that stinks.
Controllers are just like they are in other full stack frameworks, functions that glue your models and your views together.
lein controller/gen posts
Gives you this:
It’s all regular forms POSTing form data to the server and then redirecting. Just like pappy used to do! I might try to make a generator for RESTful or maybe GraphQL endpoints in the future, but for now, things are just going to have to be fast and simple.
Usually I just pass the whole request map into the view functions, which kind of kills separation of concerns, but I’m ok with that for some reason. Who’s concerns are they anyway, am I right? Here’s what a generated lein view/gen posts
view file looks like:
Pretty basic 💁♀️
This is a good starting point for something like a “full stack” clojure framework. Definitely not as robust or simple as something like luminus, but it might just be easier mostly because it’s early days and there’s no clojurescript (or javascript framework, which is a huge plus) and just one supported database… there’s a lot to do still. I don’t have a grand plan for this thing other than I want there to be multiple options when considering clojure web development, not just luminus, libraries or GTFO 👋. Hopefully coast on clojure won’t be the simplest option, but it might just be the easiest option.
I’ve already made a few sites with this framework, of course none of them have had any traction yet 😢 (that’s classic me), but it works for the most part. Give it a shot!
Coding clojure is great, but sometimes you just need to get outside and disconnect. That’s why I’m making outsidelist.com (using coast), it’s a list of the best places to get away, get out of your comfort zone and get outside.