Clojure web app: How are files organized & what are namespaces all about?

ClojureHub
7 min readDec 7, 2021

Here’s all the basics you need to know

When I first started learning how to create web apps with Clojure and ClojureScript, I had actually never made a web app before with any other language either. I made a lot of little mistakes that were really frustrating. Having my files organized incorrectly and trying to understand namespaces causes a lot of issues for me. But, I finally understand these topics now and can write about what I learned so that you don’t have to make the same mistakes I did.

Let’s get started with a simple example of a basic web app, which can be generated using a template.

We will create one with the Luminus template, and add http-kit, postgres and shadow-cljs, just like in my Basic App Setup post.

If you haven’t done that part yet, all you have to do is create a quick app with these commands and you’ll be ready to follow along:

lein new luminus cool-app +shadow-cljs +postgres +http-kit
cd cool-app

At the time of writing, the version I’m using is the latest, which is 4.20. If your files look different, it might be because your latest version is later than mine. If that’s the case and you want to follow what’s written here, just add one more term to your command:

lein new luminus cool-app --template-version 4.20 +shadow-cljs +postgres +http-kit

Let’s take a look at some of the files and directories that have been generated and what it all means. Note that I won’t be covering all the files and directories, some of which are important for more advanced topics. For the sake of clarity and brevity, I’m just going to cover the most relevant files for the topics covered in this book.

How I look at my folders is through Finder, but you can also do this in Emacs (send a message if you want me to go into more detail on how to do it in Emacs at drmiu@miuminati.com).

I open Finder, and search for cool-app. There should be a folder called cool-app that shows up.

Now when I click on the folder, I can see everything that’s been generated. I think folders are usually referred to as directories, and I’ll use those terms interchangably.

cool-app contents:

dev-config.edn

This is where you list which port you want to use on localhost, and by default with the lein template it’s going to be 3000. You can change it to a different number if you’d like. Apparently port numbers can be anything from 0 to 65353, but 0 to 1023 are reserved for common applications.

This file also contains the information for accessing your database of choice.

package.json

Here is where some dependencies are listed for the front-end part of our app:

{
"devDependencies": {
"shadow-cljs": "^2.14.3"
},
"dependencies": {
"react": "17.0.1",
"react-dom": "17.0.1"
}
}

project.clj

This file is where you list all your clj dependencies. You will also end up listing other important information, like source paths, uberjar information and resource paths.

(defproject cool-app "0.1.0-SNAPSHOT"  :description "FIXME: write description"
:url "http://example.com/FIXME"
:dependencies [[ch.qos.logback/logback-classic "1.2.6"]
[clojure.java-time "0.3.3"]
[com.cognitect/transit-clj "1.0.324"]
[com.cognitect/transit-cljs "0.8.269"]
[conman "0.9.1"]
[cprop "0.1.19"]
[expound "0.8.10"]
[funcool/struct "1.4.0"]
[json-html "0.4.7"]
[luminus-http-kit "0.1.9"]
[luminus-migrations "0.7.1"]
[luminus-transit "0.1.2"]
[luminus/ring-ttl-session "0.3.3"]
[markdown-clj "1.10.6"]
[metosin/muuntaja "0.6.8"]
[metosin/reitit "0.5.15"]
[metosin/ring-http-response "0.9.3"]
[mount "0.1.16"]
[nrepl "0.8.3"]
[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.10.879" :scope "provided"]
[org.clojure/core.async "1.3.622"]
[org.clojure/tools.cli "1.0.206"]
[org.clojure/tools.logging "1.1.0"]
[org.postgresql/postgresql "42.2.23"]
[org.webjars.npm/bulma "0.9.3"]
[org.webjars.npm/material-icons "1.0.0"]
[org.webjars/webjars-locator "0.42"]
[ring-webjars "0.2.0"]
[ring/ring-core "1.9.4"]
[ring/ring-defaults "0.3.3"]
[selmer "1.12.44"]
[thheller/shadow-cljs "2.15.12" :scope "provided"]
;;new
[reagent "1.0.0"]
[re-frame "1.1.2"]
[hiccup "2.0.0-alpha2"]
]
:min-lein-version "2.0.0"

:source-paths ["src/clj" "src/cljs" "src/cljc"]
:test-paths ["test/clj"]
:resource-paths ["resources" "target/cljsbuild"]
:target-path "target/%s/"
:main ^:skip-aot cool-app.core
:plugins []
:clean-targets ^{:protect false}
[:target-path "target/cljsbuild"]
:profiles
{:uberjar {:omit-source true

:prep-tasks ["compile" ["run" "-m" "shadow.cljs.devtools.cli" "release" "app"]]
:aot :all
:uberjar-name "cool-app.jar"
:source-paths ["env/prod/clj" "env/prod/cljs" ]
:resource-paths ["env/prod/resources"]}
:dev [:project/dev :profiles/dev]
:test [:project/dev :project/test :profiles/test]
:project/dev {:jvm-opts ["-Dconf=dev-config.edn" ]
:dependencies [[binaryage/devtools "1.0.4"]
[cider/piggieback "0.5.2"]
[org.clojure/tools.namespace "1.1.0"]
[pjstadig/humane-test-output "0.11.0"]
[prone "2021-04-23"]
[ring/ring-devel "1.9.4"]
[ring/ring-mock "0.4.0"]]
:plugins [[com.jakemccrary/lein-test-refresh "0.24.1"]
[jonase/eastwood "0.3.5"]
[cider/cider-nrepl "0.26.0"]]


:source-paths ["env/dev/clj" "env/dev/cljs" "test/cljs" ]
:resource-paths ["env/dev/resources"]
:repl-options {:init-ns user
:timeout 120000}
:injections [(require 'pjstadig.humane-test-output)
(pjstadig.humane-test-output/activate!)]}
:project/test {:jvm-opts ["-Dconf=test-config.edn" ]
:resource-paths ["env/test/resources"]


}
:profiles/dev {}
:profiles/test {}})

resources

Within this folder, are the following directories: docs, html, migrations, public and sql. Primarily we’ll use this to keep our html files, css files (in the public folder), database migrations, and sql queries.

shadow-cljs.edn

This is similar to the project.clj file, but where you’re going to list some information specifically for shadow-cljs.

{:nrepl {:port 7002}
:builds
{:app
{:target :browser
:output-dir "target/cljsbuild/public/js"
:asset-path "/js"
:modules {:app {:entries [cool-app.app]}}

:release {}}
:test {:target :node-test, :output-to "target/test/test.js"
:autorun true}}
:lein {:profile "+dev"}}

src

This folder is where the bulk of your code is going to live. There are the following folders: clj, cljc and cljs. As you’d assume, the clj files go in the clj folder and so on. The cljc folder is for certain files that are read by both the front and back end. Notice how when you click on any one of those folders, there will be another folder with your project name, and then the code lives within that folder.

I point this out because once I tried to start a project from scratch, and didn’t have the folders in the right structure, and the files weren’t able to be read correctly, so I was getting tons of errors. All because I didn’t have the files in correctly nested and titled folders (another reason it helps to use templates when you’re first learning: avoid these mistakes and familiarize yourself with a way to get things done that works).

Also, notice that the folders inside of clj, cljc and cljs are called cool_app instead of cool-app. This is necessary because of how Clojure is hosted on the JVM (Java Virtual Machine) which expects these folders to be in that format. All these folders and the files within them can be explored more thoroughly in upcoming content.

Requiring namespaces

Let’s take a look at one file for an example: src/clj/cool_app/core.clj

(ns cool-app.core
(:require
[cool-app.handler :as handler]
[cool-app.nrepl :as nrepl]
[luminus.http-server :as http]
[luminus-migrations.core :as migrations]
[cool-app.config :refer [env]]
[clojure.tools.cli :refer [parse-opts]]
[clojure.tools.logging :as log]
[mount.core :as mount])
(:gen-class))
;; log uncaught exceptions in threads
(Thread/setDefaultUncaughtExceptionHandler
(reify Thread$UncaughtExceptionHandler
(uncaughtException [_ thread ex]
(log/error {:what :uncaught-exception
:exception ex
:where (str "Uncaught exception on" (.getName thread))}))))
(def cli-options
[["-p" "--port PORT" "Port number"
:parse-fn #(Integer/parseInt %)]])
(mount/defstate ^{:on-reload :noop} http-server
:start
(http/start
(-> env
(assoc :handler (handler/app))
(update :port #(or (-> env :options :port) %))
(select-keys [:handler :host :port])))
:stop
(http/stop http-server))
(mount/defstate ^{:on-reload :noop} repl-server
:start
(when (env :nrepl-port)
(nrepl/start {:bind (env :nrepl-bind)
:port (env :nrepl-port)}))
:stop
(when repl-server
(nrepl/stop repl-server)))
(defn stop-app []
(doseq [component (:stopped (mount/stop))]
(log/info component "stopped"))
(shutdown-agents))
(defn start-app [args]
(doseq [component (-> args
(parse-opts cli-options)
mount/start-with-args
:started)]
(log/info component "started"))
(.addShutdownHook (Runtime/getRuntime) (Thread. stop-app)))
(defn -main [& args]
(-> args
(parse-opts cli-options)
(mount/start-with-args #'cool-app.config/env))
(cond
(nil? (:database-url env))
(do
(log/error "Database configuration not found, :database-url environment variable must be set before running")
(System/exit 1))
(some #{"init"} args)
(do
(migrations/init (select-keys env [:database-url :init-script]))
(System/exit 0))
(migrations/migration? args)
(do
(migrations/migrate args (select-keys env [:database-url]))
(System/exit 0))
:else
(start-app args)))

Notice the very beginning of the file:

(ns cool-app.core
(:require
[cool-app.handler :as handler]

Then, later in the file, we see handler show up again in a function:

(mount/defstate ^{:on-reload :noop} http-server
:start
(http/start
(-> env
(assoc :handler (handler/app))
(update :port #(or (-> env :options :port) %))
(select-keys [:handler :host :port])))
:stop
(http/stop http-server))

The handler namespace is called in the expression handler/app.

Let’s look at the file src/clj/cool_app/handler.clj and see what handler/app is referring to.

(ns cool-app.handler
(:require
[cool-app.middleware :as middleware]
[cool-app.layout :refer [error-page]]
[cool-app.routes.home :refer [home-routes]]
[reitit.ring :as ring]
[ring.middleware.content-type :refer [wrap-content-type]]
[ring.middleware.webjars :refer [wrap-webjars]]
[cool-app.env :refer [defaults]]
[mount.core :as mount]))
(mount/defstate init-app
:start ((or (:init defaults) (fn [])))
:stop ((or (:stop defaults) (fn []))))
(mount/defstate app-routes
:start
(ring/ring-handler
(ring/router
[(home-routes)])
(ring/routes
(ring/create-resource-handler
{:path "/"})
(wrap-content-type
(wrap-webjars (constantly nil)))
(ring/create-default-handler
{:not-found
(constantly (error-page {:status 404, :title "404 - Page not found"}))
:method-not-allowed
(constantly (error-page {:status 405, :title "405 - Not allowed"}))
:not-acceptable
(constantly (error-page {:status 406, :title "406 - Not acceptable"}))}))))
(defn app []
(middleware/wrap-base #'app-routes))

Now we can see the app function:

(defn app []
(middleware/wrap-base #'app-routes))

So in the core.clj file, handler/app referrs to this function defined in handler.clj, which was required by the core.clj file. If there was a typo in the namespace requirements in core.clj or if the requirement was left out, errors would come up because that file wouldn’t have any information about what handler/app is.

If you are working directly in the repl, and you want to use a namespace, you’ll also have to require it using the same commands in the repl.

This is a brief overview of namespaces. They can take some practice and getting used to, and they get easier to work with over time.

Conclusion

This is a brief overview of some of the important files, folders and how they are structured. I suggest exploring the files a bit on your own to familiarize yourself with how they can be set up in a way that works. Want to see more? Head over to my youtube channel for video coding tutorials and my pet project for more educational coding related content!

--

--

ClojureHub

Follow us for beginner Clojure web app tutorials & content. See more at clojurehub.com!