Getting Started

You're going to need to install these three things.

I'll wait...



Alright! We should be good to go! These docs are also kind of a tutorial. Let's make a blog! Run this from your terminal
~ lein new coast blog
~ cd blog
~/blog tree
Check out those sweet, sweet files!
├── Procfile
├── profiles.clj
├── project.clj
├── resources
│   └── public
│       ├── css
│       │   └── app.css
│       └── js
│           └── app.js
├── src
│   └── blog
│       ├── components.clj
│       ├── controllers
│       │   ├── errors_controller.clj
│       │   └── home_controller.clj
│       ├── core.clj
│       ├── routes.clj
│       └── views
│           ├── errors.clj
│           └── home.clj
└── test
    └── blog
        └── core_test.clj
What do the files do though?
  • Procfile
    Used by heroku or foreman to start your app
    Used by you four months from now to figure how to pick up where your lazy butt left off
  • profiles.clj
    This is where your dev environment variables are stored, things like server port, database connection and cookie hashing secret. Shhh
  • project.clj
    This is where project specific stuff like the version of your app, dependencies and any coast helper functions (in the form of lein aliases) are stored
  • resources/public
    This is where any public facing file like js, css, favicon or robots.txt
  • resources/migrations
    This doesn't exist yet, but this is where the migrations will go
  • src/blog/components.clj
    This is where re-usable bits of html go. They're named components because it's trendy. No fomo
  • src/blog/controllers
    This is where functions that glue your html to your views and your database go
  • src/blog/core.clj
    This is where your app startup code, middleware and main function go
  • src/blog/routes.clj
    This is where all of your app's urls are stored
  • src/blog/views
    This is where your html templates (kind of) live
  • test/blog/core_test.clj
    This is where you can test your whole app just one step before rendering to headless chrome or something


Time to make a new dev database and a posts table, we're still in the terminal here. This will create a new postgres database (assuming a running postgres server), also coast only currently assumes postgres, so hopefully that works for you!
~/blog lein db/create
Here's how you can make new tables, what's sweet is that coast tries to fill in your migration file for you when you use "create-x" as the migration name where "x" is the table being created. This should make a new file named _create_posts.sql in resources/migrations
~/blog lein db/migration create-posts title:text body:text
resources/migrations/_create_posts.sql created
Let's check out what that file looks like.
-- up
create table posts (
  id serial primary key,
  title text,
  body text,
  created_at timestamp without time zone default (now() at time zone 'utc')

-- down
drop table posts
No special anything, just sql. Pretty straightforward. Run migrations like this:
~/blog lein db/migrate
_create_posts migrated successfully


This next command generates static sql that coast can turn into functions
~/blog lein sql/gen posts
Here's what that looks like:
-- name: all
select *
from posts
order by created_at desc

-- name: find-by-id
-- fn: first
select *
from posts
where id = :id

-- name: where
where id = :id
returning *
Oh my gosh static sql, someone help me. I'm drowning. Let's hook it up to clojure functions
~/blog lein model/gen posts
Here's what that looks like:
(ns blog.models.posts
  (:require [coast.db :as db])
  (:refer-clojure :exclude [update]))

(def columns [:title :body])

(defn all []
  (db/query :posts/all))

(defn find-by-id [id]
  (db/query :posts/find-by-id {:id id}))

(defn insert [m]
  (->> (select-keys m columns)
       (db/insert :posts)))

(defn update [id m]
  (as-> (select-keys m columns) %
        (db/update :posts % :posts/where {:id id})))

(defn delete [id]
  (db/delete :posts :posts/where {:id id}))
Thank goodness it's not all sql everywhere, right? Phew! 😅

So, here's what's going down. Writes are generated automatically (except for the where clause) and all queries are static sql. This has worked for me so far, hopefully it'll work for you too!

On to views!


~/blog lein view/gen posts
Here's the result:
(ns blog.views.posts
  (:require [blog.components :as c]
            [coast.core :as coast]))

(defn post [m]
  (let [{:keys [id title body created-at]} m]
     [:td id]
     [:td title]
     [:td body]
     [:td created-at]
      (coast/link-to "Edit" ["/posts/:id/edit" m])]
      (coast/link-to "Delete" [:delete "/posts/:id" m])]
      (coast/link-to "Show" ["/posts/:id" m])]]))

(defn index [request]
  (let [{:keys [posts]} request]
         [:th "id"]
         [:th "title"]
         [:th "body"]
         [:th "created-at"]
         (for [m posts]
           (post m))]]
       (coast/link-to "New post" ["/posts/fresh"])]]))

(defn show [request]
 (let [{:keys [post]} request
       {:keys [id title body created-at]} post]
     [:div id]
     [:div title]
     [:div body]
     [:div created-at]
       (coast/link-to "Delete" [:delete "/posts/:id" post])]
       (coast/link-to "Back" ["/posts"])]]))

(defn fresh [request]
  (let [{:keys [post error]} request
        {:keys [title body]} post]
      (coast/form-for [:post "/posts"]
         [:label "title"]
         [:input {:type "text" :name "title" :value title}]]
         [:label "body"]
         [:input {:type "text" :name "body" :value body}]]
          [:input {:type "submit" :value "Save"}]])
       (coast/link-to "Back" ["/posts"])]]))

(defn edit [request]
  (let [{:keys [post error]} request
        {:keys [title body]} post]
      (coast/form-for [:put "/posts/:id" post]
         [:label "title"]
         [:input {:type "text" :name "title" :value title}]]
         [:label "body"]
         [:input {:type "text" :name "body" :value body}]]
         [:input {:type "submit" :value "Save"}]])
       (coast/link-to "Back" ["/posts"])]]))
So this is cool. It's mostly just hiccup except for a few helper functions like link-to and form-for, which are pretty straightforward,
they each take a vector that looks like [http-method, url pattern, a clojure map with keys matching the url] It's a little better than something like this:

[:a {:href (str "/users/" (:id m))}]

And it's all just regular clojure functions that take ring request maps that come from...


~/blog lein controller/gen posts
Without further ado:
(ns blog.controllers.posts-controller
  (:require [coast.core :as coast]
            [blog.models.posts :as posts]
            [blog.views.posts :as views.posts]))

(defn index [request]
  (let [posts (posts/all)]
    (views.posts/index (assoc request :posts posts))))

(defn show [request]
  (let [id (get-in request [:params :id])
        post (posts/find-by-id id)]
    (views.posts/show (assoc request :post post))))

(defn fresh [request]
  (views.posts/fresh request))

(defn create [request]
  (let [params (get request :params)
        [post error] (coast/try! (posts/insert params))]
    (if (nil? error)
      (coast/redirect "/posts")
      (fresh (assoc request :error error)))))

(defn edit [request]
  (let [id (get-in request [:params :id])
        post (posts/find-by-id id)]
    (views.posts/edit (assoc request :post post))))

(defn change [request]
  (let [params (get request :params)
        id (get params :id)
        [post error] (coast/try! (posts/update id params))]
    (if (nil? error)
      (coast/redirect "/posts")
      (edit (assoc request :error error)))))

(defn delete [request]
  (let [id (get-in request [:params :id])
        [post error] (coast/try! (posts/delete id))]
    (coast/redirect "/" error)))
If this seems familiar, it's because it is! It's the 7 deadly RESTful-ish-kind-of functions from frameworks like rails!

Hopefully this is straightforward, that's the goal anyway...

(defn index [request])
This function returns a list of stuff

(defn show [request])
This function shows one of something

(defn fresh [request])
This function is the same as rails /new route except new is a reserved word or something
in clojure so I used fresh instead. It shows a form to create a new row in the database.

(defn create [request])
This function routes to a POST function when a form is submitted

(defn edit [request])
This function is similar to fresh, except it shows a form for existing rows

(defn change [request])
This function is called change and not update because update is a core clojure function so whatever. It's another form POST with a special put parameter that's supposed to be a faux RESTful thing.
I still like REST, come at me.

(defn delete [request])
This function is just a get request with a delete parameter so the REST stuff is still in tact.


If that last section on controllers was confusing, this will probably clear it up
(ns blog.routes
  (:require [coast.core :as coast]
            [blog.controllers.errors-controller :as errors]
            [blog.controllers.posts-controller :as posts]))

(def routes
  (-> (coast/get "/posts" posts/index)
      (coast/get "/posts/fresh" posts/fresh)
      (coast/get "/posts/:id/edit" posts/edit)
      (coast/get "/posts/:id" posts/show)
      (coast/post "/posts" posts/create)
      (coast/put "/posts/:id" posts/change)
      (coast/delete "/posts/:id" posts/delete)
      (coast/route-not-found errors/not-found)))

The routes.clj file maps functions from the controller to urls and that completes the circle of web. 🦁

Oh there's also a way to say what happens when there are no routes (route-not-found)

So you could do this, right, type out all 7 functions and all 7 routes, OR you can use RESOURCE ROUTING... ROUTING... ROUTING... and yes this was shamelessly stolen from rails, great artists amirite?

(ns blog.routes
  (:require [coast.core :as coast]
            [blog.controllers.errors-controller :as errors]
            [blog.controllers.posts-controller :as posts]))

(def routes
  (-> (coast/resource :posts)
      (coast/route-not-found errors/not-found)))
Those same routes with resource routing. Less typing, everybody wins.