Complete Web Development Setup Using Clojure CLI Tools

1,920 views
Skip to first unread message

Gary Johnson

unread,
Jul 9, 2018, 12:14:22 PM7/9/18
to Clojure
Howdy Clojurians,

I recently started developing a new Clojure+Clojurescript web application, and I wanted to see if I could set up my development environment using just the Clojure CLI tools. After a good deal of digging around through tutorials on a number of different websites and a fair amount of experimenting, I've managed to create a very simple (IMHO) configuration that provides me with both development and production mode CLJS->JS compilation, development and production mode ring handlers, and the always delightful FIgwheel development environment all from just the simple "clojure" command. Since I haven't seen this before, I thought I'd share it with all of you in case it helps someone else out there who doesn't need (or want) all of leiningen or boot to develop a simple web app.

Here goes:

Step 1: Create your project structure like so:

├── cljsbuild.edn
├── deps.edn
├── figwheel.edn
├── resources
│   └── public
│       ├── cljs
│       ├── css
│       │     ├── style.css
│       ├── images
│       └── js
├── src
│   ├── clj
│   │   └── my_project
│   │       ├── handler.clj
│   │       ├── server.clj
│   │       ├── views.clj
│   └── cljs
│       └── my_project
│           ├── client.cljs

Step 2: Make the deps.edn file (replace :deps and my-project.server namespace as necessary for your project)

{:paths ["src/clj" "resources"]

 
:deps {org.clojure/clojure       {:mvn/version "1.9.0"}
        org
.clojure/clojurescript {:mvn/version "1.10.312"}
        ring                      
{:mvn/version "1.7.0-RC1"}
        ring
/ring-defaults        {:mvn/version "0.3.2"}
        prone                    
{:mvn/version "1.6.0"}
        compojure                
{:mvn/version "1.6.1"}
        hiccup                    
{:mvn/version "1.0.5"}
        reagent                  
{:mvn/version "0.8.1"}}

 
:aliases {:run        {:main-opts ["-m" "my-project.server"]}
           
:cljsbuild  {:extra-paths ["src/cljs"]
                       
:main-opts ["-m" "cljs.main" "-co" "cljsbuild.edn" "-c"]}
           
:figwheel   {:extra-deps {org.clojure/tools.nrepl {:mvn/version "0.2.13"}
                                     cider
/cider-nrepl       {:mvn/version "0.17.0"}
                                     com
.cemerick/piggieback {:mvn/version "0.2.2"}
                                     figwheel
-sidecar        {:mvn/version "0.5.14"}}
                       
:main-opts ["-e" "(use,'figwheel-sidecar.repl-api),(start-figwheel!)"]}}}


Step 3: Make the cljsbuild.edn file (replace :main for your project)

{:main          "my-project.client"
 
:output-dir    "resources/public/cljs"
 
:output-to     "resources/public/cljs/app.js"
 
:source-map    "resources/public/cljs/app.js.map"
 
:optimizations :advanced
 
:pretty-print  false}

Step 4: Make the figwheel.edn file (replace :ring-handler, :on-jsload, and :main for your project)

{:nrepl-port       7000
 
:nrepl-middleware ["cider.nrepl/cider-middleware"
                   
"cemerick.piggieback/wrap-cljs-repl"]
 
:server-port      3000
 
:ring-handler     my-project.handler/development-app
 
:http-server-root "public"
 
:css-dirs         ["resources/public/css"]
 
:builds [{:id           "dev"
           
:source-paths ["src/cljs"]
           
:figwheel     {:on-jsload "my-project.client/mount-root"}
           
:compiler     {:main          "my-project.client"
                         
:output-dir    "resources/public/cljs/out"
                         
:output-to     "resources/public/cljs/app.js"
                         
:asset-path    "/cljs/out"
                         
:source-map    true
                         
:optimizations :none
                         
:pretty-print  true}}]}


Step 5: Write server.clj

(ns my-project.server
 
(:require [ring.adapter.jetty :refer [run-jetty]]
           
[my-project.handler :refer [development-app production-app]])
 
(:gen-class))

(defonce server (atom nil))

(defn start-server! [& [port mode]]
 
(reset! server
         
(run-jetty
           
(case mode
             
"dev"  #'development-app
             
"prod" #'production-app
             
#'production-app)
           
{:port (if port (Integer/parseInt port) 3000)
           
:join? false})))

(defn stop-server! []
 
(when @server
   
(.stop @server)
   
(reset! server nil)))

(def -main start-server!)


Step 6: Write handler.clj

(ns my-project.handler
 
(:require [ring.middleware.defaults :refer [wrap-defaults site-defaults]]
           
[ring.middleware.reload :refer [wrap-reload]]
           
[prone.middleware :refer [wrap-exceptions]]
           
[compojure.core :refer [defroutes GET]]
           
[compojure.route :refer [not-found]]
           
[my-project.views :refer [render-page]]))

(defroutes routes
 
(GET "/" [] (render-page))
 
(not-found "Not Found"))

(def development-app (wrap-reload
                     
(wrap-exceptions
                       
(wrap-defaults #'routes site-defaults))))

(def production-app (wrap-defaults #'routes site-defaults))


Step 7: Write views.clj

(ns my-project.views
 
(:require [hiccup.page :refer [html5 include-css include-js]]))

(defn render-page []
 
(html5
   
[:head
   
[:title "My Project"]
   
[:meta {:charset "utf-8"}]
   
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
   
(include-css "/css/style.css")
   
(include-js "/cljs/app.js")]
   
[:body
   
[:div#app]
   
[:script {:type "text/javascript"} "my_project.client.mount_root();"]))



Step 8: Write client.cljs (replace Reagent with whichever front-end library you prefer)

(ns my-project.client
 
(:require [reagent.core :as r]))

(defn root-component []
 
[:div [:h1 "Hello world!"]])

(defn ^:export mount-root []
 
(r/render [root-component]
           
(.getElementById js/document "app")))


Step 9: Write some CSS in resources/public/css/style.css

#app {
  border
: 2px solid green;
}

Step 10: Try out your new dev tools!

At this point, your project setup is complete, and you are ready to start developing your awesome new Clojure+Clojurescript web app. You have the following 3 project management commands available at your command prompt:

1. Compile Clojurescript to Javascript

To compile the Clojurescript files under src/cljs to Javascript under resources/public/cljs, navigate to the toplevel project directory and run:

$ clojure -A:cljsbuild

The main Javascript entry point file will be written to resources/public/cljs/app.js. The Clojurescript build options are read from the toplevel cljsbuild.edn file. They are set to use advanced compilation mode for a production build.


2. Run your Web Application

To compile and run your web application, navigate to the toplevel project directory and run:

$ clojure -A:run [port] [dev|prod]

The website will then be available at http://localhost:3000 or on whichever port you specified. In dev mode, server-side exceptions will be displayed in the browser and Clojure source files will be reloaded whenever you refresh the page. These features are disabled in prod mode. If the second argument to run is omitted, it will default to prod mode.


3. Launch Figwheel

To start the Figwheel server, navigate to the toplevel project directory and run:

$ clojure -A:figwheel

This will start an http-kit webserver on http://localhost:3000, which serves up the website in dev mode. It will also open an nREPL on port 7000, which provides the special command "(cljs-repl)" to switch from a Clojure REPL to a Clojurescript REPL. Finally, any changes to CLJS or CSS files will automatically be pushed to the browser when the
files are saved.


Okay, folks. That's all from me for now. Setting this all up was quite an interesting learning exercise, but I'm very happy with the results. I hope someone out there finds this setup useful for your next Clojure project. If someone has the power to add this tutorial to the Clojure website (or other documentation site), I think it would be a great addition.

Also, someone should definitely make a clj-new project template from this setup. I'm looking at you, Sean Corfield. ;-D

Have fun, everyone, and happy hacking!

~Gary

Gary Johnson

unread,
Jul 9, 2018, 4:11:46 PM7/9/18
to Clojure
Hi again, folks. Just to make it easier for everyone to use this template right away, I put all these files into a public git repository on Gitlab. Here's the URL:

    https://gitlab.com/lambdatronic/clojure-webapp-template

Happy hacking!
~Gary

Gary Johnson

unread,
Jul 10, 2018, 10:29:49 AM7/10/18
to Clojure
For those of you playing along at home, you may have noticed that there were two bugs in the code I presented above. I have since fixed those issues in the Gitlab repository that I linked to in my previous post. If you didn't just grab the repository, here are the fixes for you to add manually to your setups:

Fix 1: Corrected deps.edn

{:paths ["src/clj" "src/cljs" "resources"]


 
:deps {org.clojure/clojure       {:mvn/version "1.9.0"}
        org
.clojure/clojurescript {:mvn/version "1.10.312"}
        ring                      
{:mvn/version "1.7.0-RC1"}
        ring
/ring-defaults        {:mvn/version "0.3.2"}
        prone                    
{:mvn/version "1.6.0"}
        compojure                
{:mvn/version "1.6.1"}
        hiccup                    
{:mvn/version "1.0.5"}
        reagent                  
{:mvn/version "0.8.1"}}

 
:aliases {:run        {:main-opts ["-m" "my-project.server"]}

           
:cljsbuild  {:main-opts ["-m" "cljs.main" "-co" "cljsbuild.edn" "-c"]}

           
:figwheel   {:extra-deps {org.clojure/tools.nrepl {:mvn/version "0.2.13"}
                                     cider
/cider-nrepl       {:mvn/version "0.17.0"}
                                     com
.cemerick/piggieback {:mvn/version "0.2.2"}
                                     figwheel
-sidecar        {:mvn/version "0.5.14"}}
                       
:main-opts ["-e" "(use,'figwheel-sidecar.repl-api),(start-figwheel!)"]}}}

The issue was that leaving src/cljs out of the :paths vector and adding it with :extra-paths in the :cljsbuild alias had left the :figwheel alias unable to load CLJS files at the CLJS REPL that it spawns. The above code corrects this problem.

Fix 2: Corrected views.clj


(ns my-project.views
 
(:require [hiccup.page :refer [html5 include-css include-js]]))

(defn render-page []
 
(html5
   
[:head
   
[:title "My Project"]
   
[:meta {:charset "utf-8"}]
   
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
   
(include-css "/css/style.css")
   
(include-js "/cljs/app.js")]
   
[:body
   
[:div#app]
   
[:script {:type "text/javascript"} "my_project.client.mount_root();"]]))


In my initial post, I accidentally left out a closing square bracket in the render-page function. This block of code adds it back in.


Okay, folks. That's it for now. Once again, you can just grab the corrected template from this Gitlab link if you are so inclined:

  https://gitlab.com/lambdatronic/clojure-webapp-template

Over and out,
  Gary

Chad Stovern

unread,
Jul 11, 2018, 11:03:55 AM7/11/18
to Clojure
This is awesome Gary thank you for sharing!  I just finished a toy project that is also full stack with a leiningen setup so it was interesting to compare.  I could see this being easy to extend to also add a :garden alias to compile css as well in a clj/css.clj file.

Keep up the awesome work!

Gary Johnson

unread,
Jul 17, 2018, 11:40:38 AM7/17/18
to Clojure
Thanks, Chad.

I have built quite a few toy and production full-stack Clojure web apps over the past 6 years or so using leiningen and boot. While both of these are great tools with a lot of programmer hours invested in them, I realized recently that neither of them are particularly easy to explain to a novice Clojure programmer, particularly when you are using them to configure a full stack web development environment.

Since I hadn't yet tried doing any web dev with the Clojure CLI tools, this seemed like a good test project to get my feet wet. Despite the seemingly stripped-down set of options available in deps.edn (just :paths, :deps, and :aliases), it turned out that the :aliases feature really provides all you need to bootstrap a wide variety of build tasks directly into the clojure command.

What I really like best about this approach is that I can now introduce new programmers to Clojure using command line conventions that they are likely already familiar with coming from many other popular languages like perl, python, ruby, or node. So now I can just type:

```
$ clojure
```

and get a REPL. Or I could type:

```
$ clojure script.clj
```

and I'm running a simple single-file Clojure program. If I need something more complicated, I can just define an alias for a build task a la carte and type:

```
$ clojure -A:my-task
```

Because taking this approach makes Clojure development look almost identical to building a program in any of the other languages I mentioned (and many more that I didn't), it makes the tooling very "easy" (in the Rich sense) for newbies that I need to train in our company. And when I want to play power-programmer, I get to have fun just cranking out new aliases for adventure and profit!

And hey, if you've read along this far, here's a free alias for you to play with or modify to suit your needs:

```
:minify-css {:extra-deps {asset-minifier {:mvn/version "0.2.5"}}
                        :main-opts ["-e" "(use,'asset-minifier.core),(minify-css,\"resources/public/css/style.css\",\"resources/public/css/style.min.css\")"]}
```

Have fun and happy hacking!
  Gary

Tim Goodwin

unread,
Jul 18, 2018, 5:00:59 AM7/18/18
to Clojure
for additional inspiration, i suggest looking at https://github.com/juxt/edge

Gary Johnson

unread,
Jul 20, 2018, 12:03:36 PM7/20/18
to Clojure
Thanks for the link!

Jag Gunawardana

unread,
Apr 27, 2020, 8:24:22 AM4/27/20
to Clojure
Thanks Gary for posting this up. Was a great help for a slightly different use case. I was using static site generators like Hugo, but always found that I ended up having to learn their internals/templating to make larger changes. I also wanted to use Clojurescript and it was painful to do so. I made a few changes:

I used the Stasis library to turn my hiccup and assets into a static site (but serve up with Ring in development).
I used shadow-cljs to compile my Clojurescript.
I also use deps.edn rather than Lein (even though Lein still feels easier).
My final artifact was a largely static website, packaged up into an Nginx docker. I have a small amount of clojurescript which gets built into one main.js file.

I can post up the details/a repo if anyone else wants to do similar.

I always feel that you can do pretty much anything with Clojure(script), but sometimes there is a bit of a dark art to getting there. I think I would use this approach for any site that I would have used a static site generator for now. 


On Monday, 9 July 2018 17:14:22 UTC+1, Gary Johnson wrote:

raybaq

unread,
Apr 27, 2020, 7:50:03 PM4/27/20
to Clojure
Hi Jag,

Would love to learn more about your approach.  I have just gotten started with shadow-cljs.  I read about Stasis some time ago but havent actually used it.

TIA,

Ray

Blake Miller

unread,
Apr 29, 2020, 2:37:45 PM4/29/20
to Clojure
Jag,

I think what you described is worth sharing. I like the simplicity of that approach and the efficiency of the final artifact.

-Blake

Jag Gunawardana

unread,
Apr 30, 2020, 6:01:21 AM4/30/20
to Clojure
Quick background:

I often use static site generators for landing pages, marketing pages etc. Hugo is normally my goto product for this as the themes are good and it is easy enough to change them, but recently I felt that there has to be a better way when you want something (just slightly) beyond a static page. I also have limited time to learn things, and my Go templating was very rusty. I use more Clojure(script) these days, so thought that there must be a simple way to generate static pages from something like hiccup, with a bit of Clojurescript interaction where needed. I also wanted to have a sensible dev setup so that I wouldn't have to constantly stop and start servers. Finally I've tried to move to shadow cljs for Clojurescript, and I've tried to use deps.edn rather than lein/boot, so I wanted to keep doing this. I started with looking at Juxt Edge as I've had some success with this for servers in the past, but it seemed a bit heavy handed for my requirements. I found the following very helpful pages:

and post that started this discussion above.

I have the following directory structure:

src/clj/mypages    for Clojure
src/cljs/mypages  for Clojurescript  
resources/public/css
resources/public/js
resources/public/images

Using the following:
deps.edn

{:paths ["src/clj" "resources" "src/cljs"]
 :deps
 {org.clojure/clojure       {:mvn/version "1.10.1"}
  org.clojure/clojurescript {:mvn/version "1.10.597"}
  ring                      {:mvn/version "1.8.0"}
  ring/ring-defaults        {:mvn/version "0.3.2"}
  hiccup/hiccup             {:mvn/version "1.0.5"}
  stasis                    {:mvn/version "2.5.0"}
  optimus                   {:mvn/version "0.20.2"}
  com.taoensso/timbre       {:mvn/version "4.10.0"}}

 :aliases
 {:cljs
  {:extra-deps {thheller/shadow-cljs {:mvn/version "2.8.93"}}
   :main-opts ["-m" "shadow.cljs.devtools.cli" "release" "build"] }
  :run
  {:main-opts ["-m" "mypages.server"]}
  :build
  {:main-opts ["-m" "mypages.static"]}}}

shadow-cljs.edn

;; shadow-cljs configuration
{:source-paths
 ["src/cljs"]

 :dependencies []

 :builds
 {:build     {:target     :browser
              :output-dir "resources/public/js/"
              :asset-path "/public//js/"
              :modules    {:main {:init-fn mypages.helpers/init}}
              :dev        {:devtools        {:repl-pprint true}}
              :devtools   {:console-support false}}}}

some standard ring code for development web server:

src/clj/mypages/server.clj

(ns mypages.server
  (:require [ring.adapter.jetty :refer [run-jetty]]
            [ring.middleware.reload :refer [wrap-reload]]
            [taoensso.timbre :as log]
            [mypages.pages.web :refer [dev-app prod-app]]))

(defonce server (atom nil))

(defn start-server! [& [port mode]]
  (log/debug "Mode: " mode)
  (reset! server
          (run-jetty
           (case mode
             "dev" (wrap-reload #'dev-app)
             "prod" (wrap-reload #'prod-app)
             (wrap-reload #'dev-app))
           {:port (if port (Integer/parseInt port) 8080)
            :join? false})))

(defn stop-server! []
  (when @server
    (.stop @server)
    (reset! server nil)))

(def -main start-server!)

stasis needs some code to help generate the markup, and we use optimus to tidy up, optimise and serve assets. I only serve some css out of the resources/public dir, but you could dump some html in there is you wanted to. This is in src/clj/mypages/pages/web.clj

(ns mypages.pages.web
  (:require [stasis.core :as stasis]
            [optimus.assets :as assets]
            [optimus.optimizations :as optimizations]
            [optimus.prime :as optimus]
            [optimus.strategies :refer [serve-live-assets]]
            [mypages.pages.layout :as layout]))

(defn home-page [request]
  (layout/landing-page request))

(defn get-assets []
  (assets/load-assets "public" [#".*\.(jpg|svg|png|js)$"]))

(defn get-public []
  (merge (stasis/slurp-directory "resources/public" #".*\.(html|css)$" :encoding "UTF-8")
         {"/" home-page}))

(defn get-dev []
  (merge (get-public)
         {"/signup/" layout/redirect}))

(defn create-app [f]
  (optimus/wrap
   (stasis/serve-pages f)
   get-assets
   optimizations/all
   serve-live-assets))

(def dev-app
  (create-app get-dev))

(def prod-app
  (create-app get-public))

I have some template like functions that allow me to wrap the content (which I could possibly write in Markdown later e.g. blog posts) in src/clj/mypages/pages/layout.clj:

(ns mypages.pages.layout
  (:require [hiccup.page :refer [html5]]
            [stasis.core :as stasis]))

(defn redirect [request]
  (html5
   [:head
    [:meta {:http-equiv "refresh" :content "0; URL='https://server.com/auth/signup'"}]]))

(defn landing-page [request content]
  (html5
   [:head
    [:meta {:charset "utf-8"}]
    [:meta {:name    "viewport"
            :content "width=device-width, initial-scale=1.0"}]
    [:title "MyPages"]
    [:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/bu...@0.8.2/css/bulma.min.css"}]
    [:link {:rel "stylesheet" :href "/css/styles.css"}]
    [:script {:type "text/javascript" :src "/js/main.js"}]
    [:script {:defer true :src "https://use.fontawesome.com/releases/v5.3.1/js/all.js"}]]
   [:body [:p content]])

Then some code to generate the static pages when I want them in src/clj/mypages/static.clj

(ns mypages.static
  (:require
   [stasis.core :as stasis]
   [optimus.export]
   [optimus.optimizations :as optimizations]
   [mypages.pages.web :as web]))

(def export-dir "static")

(defn export! []
  (let [assets (optimizations/all (web/get-assets) {})]
    (stasis/empty-directory! export-dir)
    (optimus.export/save-assets assets export-dir)
    (stasis/export-pages (web/get-public) export-dir {:optimus-assets assets})))

(def -main export!)

Finally I have my Clojurescript in src/cljs/mypages/helpers.cljs

(ns mypages.helpers)

(defn ^:export somefunc []
  (js/alert "Not available yet .... sorry"))

(defn ^:export init []
  (js/alert "All ready now"))

To build the Clojurescript you can use either of:

npx shadow-cljs release build

or

clojure -A:cljs


To work on the static pages, use:

clojure -A:run

This will reload when you change them.

If you are making changes to the cljs regularly there is probably a neat of using the shadow-cljs dev web server and doing some sort of reload, but I haven't needed that yet, so have not done this. If anyone does, then please post up how.

I add pages and required clojurescript and once this is ready I build the static site by first building the Clojurescript (as above), then build the static pages:

clojure -A:build

I then package it all up into a Docker with nginx serving the site. I use a Makefile in the CI to do this.

The setup is fairly simple and could do with some refinement (hot reload of js would be next). I like that it gets me to something working quickly and it would be easy to being in something like Reagent if a few React components are needed. I really wanted something that was small and generated fast loading sites. I use re-frame for SPAs, but for some work this is overkill and this setup is better. I hope that this is of use, please ask any questions.

Regards
Jag

Jag Gunawardana

unread,
Apr 30, 2020, 6:06:43 AM4/30/20
to Clojure
One things I forgot. For a landing/marketing page, the main business aim is to get someone to signup/in so the difference between the prod and dev app is that for dev, I use a redirect (as per the signup link), in production I have an Nginx rule that redirects the user to an application page. As I use the same docker in all environments I specify this with an env var. Not sure anyone needs this detail, but it explains when I have to two app setups.

Jag Gunawardana

unread,
May 5, 2020, 4:16:29 PM5/5/20
to Clojure
The work above seems to be enough to get a Cider repl and code reload (from shadow-cljs) working. So if you start the ring server with clojure -A:run and then run a shadow-cljs repl on the :build id it will hot reload your cljs code.
Reply all
Reply to author
Forward
0 new messages