1. Conceptual Overview of Components
    1. Ring
    2. Compojure
    3. Hiccup
    4. H2
  2. Create and set up your project
  3. Add some styling
  4. Set up your database
  5. Create some db access functions
  6. Create your Views
  7. Set up your routes
  8. Run your webapp during development
    1. Running from the command-line
    2. Running interactively (in the REPL)
  9. Deploy your webapp
    1. Changes in deps.edn
    2. Add a build.clj file
    3. Changes in handler.clj
    4. Build and Run it
  10. See Also
  11. Contributors

This guide covers building a simple web-application using common Clojure libraries. When you're done working through it, you'll have a little webapp that displays some (x, y) locations from a database, letting you add more locations as well.

It's assumed that you're already somewhat familiar with Clojure. If not, see the Getting Started and Introduction guides.

This work is licensed under a Creative Commons Attribution 3.0 Unported License (including images & stylesheets). The source is available on Github.

This guide uses Clojure 1.11.4, as well as current versions of the component libraries noted below.

Conceptual Overview of Components

We'll use four major components (briefly described below) for our little webapp:

  • Ring
  • Compojure
  • Hiccup
  • H2

Ring

Ring (at clojars) is a foundational Clojure web application library. It:

  • sets things up such that an HTTP request comes into your webapp as a regular Clojure hashmap, and likewise makes it so that you can return a response as a hashmap.
  • provides a spec describing exactly what those request and response maps should look like.
  • brings along a web server (Jetty) and connects your webapp to it.

For this tutorial, we won't actually need to deal with these maps by-hand, as you'll soon see.

For more info, see:

Compojure

If we were using only Ring, we'd have to write one single function to take that incoming request map and then delegate to various functions depending upon which page was requested. Compojure (at clojars) provides some handy features to take care of this for us such that we can associate url paths with corresponding functions, all in one place.

For more info, see:

Hiccup

Hiccup (at clojars) provides a quick and easy way to generate html. It converts regular Clojure data structures right into html. For example,

[:p "Hello, " [:i "doctor"] " Jones."]

becomes

<p>Hello, <i>doctor</i> Jones.</p>

but it also does two extra handy bits of magic:

  • it provides some CSS-like shortcuts for specifying id and class, and

  • it automatically unpacks seqs for you, for example:

    [:p '("a" "b" "c")]
    ;; expands to (and so, is the same as if you wrote)
    [:p "a" "b" "c"]
    

For more info, see:

H2

H2 is a small and fast Java SQL database that could be embedded in your application or run in server mode. It uses a single file for storage, but also could be run as in-memory DB.

Another similar Java-based embedded DB that could be used in your application is Apache Derby.

Create and set up your project

We're going to create this project from scratch and use the Clojure CLI so you can how see how all the moving parts work.

In a new folder, perhaps called my-webapp, we're going to create a deps.edn file to specify the libraries we want to use, and a couple of folders: one for CSS files and one for your source code.

The deps.edn file should have the following contents:

{:paths ["src" "resources"]
 :deps {;; basic Ring and web server:
        ring/ring-core {:mvn/version "1.12.2"}
        ring/ring-jetty-adapter {:mvn/version "1.12.2"}
        ;; logging, required by jetty:
        org.slf4j/slf4j-simple {:mvn/version "2.0.13"}

        ;; routing:
        compojure/compojure {:mvn/version "1.7.1"}

        ;; convenient package of "default" middleware:
        ring/ring-defaults {:mvn/version "0.5.0"}

        ;; to generate HTML:
        hiccup/hiccup {:mvn/version "1.0.5"}

        ;; for the database:
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}
        com.h2database/h2 {:mvn/version "2.2.224"}}}

Now we'll create the first version of our source file:

;; this file is: src/my_webapp/handler.clj
(ns my-webapp.handler
  (:require [compojure.core :refer [defroutes GET]]
            [compojure.route :as route]
            [ring.adapter.jetty :as jetty]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes
  (GET "/" [] "Hello World")
  (route/not-found "Not Found"))

(def app
  ;; use #' prefix for REPL-friendly code -- see note below
  (wrap-defaults #'app-routes site-defaults))

(defn -main []
  (jetty/run-jetty #'app {:port 3000}))

REPL-friendly code: we use the #' prefix on var names so that we can update the definitions while the program is running, without needing to restart our program -- see writing REPL-friendly programs on clojure.org.

Note: the directory has an underscore in it (my_webapp) but the namespace has a hyphen in it (my-webapp). This is important in Clojure: we use lowercase names with hyphens to separate "words" -- often called kebab-case -- but the corresponding directory and filenames should be lowercase with underscores to separate "words" -- often called snake_case. This is due to how Clojure maps code onto names that are acceptable to the underlying JVM ecosystem.

At this point you can run this very basic web application from the command-line:

clojure -M -m my-webapp.handler

This says we want to run Clojure's main entry point (-M) and then -m my-webapp.handler tells Clojure that we want it to run the -main function in that namespace.

It will output something like this (and then "hang" while the web server is running):

[main] INFO org.eclipse.jetty.server.Server - jetty-11.0.21; built: 2024-05-14T03:19:28.958Z; git: 996cd61addad9cb033e0e3eba6fa3f0fa3dc270d; jvm 21.0.1+12-LTS
[main] INFO org.eclipse.jetty.server.handler.ContextHandler - Started o.e.j.s.ServletContextHandler@677274e7{/,null,AVAILABLE}
[main] INFO org.eclipse.jetty.server.AbstractConnector - Started ServerConnector@5570ee6d{HTTP/1.1, (http/1.1)}{0.0.0.0:3000}
[main] INFO org.eclipse.jetty.server.Server - Started Server@729d1428{STARTING}[11.0.21,sto=0] @4321ms

Note: you can stop this program running by pressing ^C (control-c) on macOS or Linux, or by pressing ^Z (control-z) on Windows.

The only relevant line in that output is Started ServerConnector where it shows the host and port it is running on -- 0.0.0.0:3000 -- so you should be able to open a web browser and go to http://localhost:3000 and you should see:

Hello World

If you go to http://localhost:3000/page you should instead see:

Not Found

This is because defroutes specifies a single route (GET "/") and then route/not-found will match all other requests and present the given string "Not Found".

Stop the program (as indicated above) and we'll add more features to it.

Add some styling

Now we're going to create some styling by creating a CSS file:

Create the folder structure resources/public/css and add a styles.css file this with contents like:

// resources/public/css/styles.css
body {
    background-color: Cornsilk;
}

#header-links {
    background-color: BurlyWood;
    padding: 10px;
}

h1 {
    color: CornflowerBlue;
}

Set up your database

A file containing the database would be automatically created when you connect to it for the first time, so all necessary database preparations could be done programmatically using the REPL (with help of next.jdbc):

clj

Execute the following code to create a new my-db.mv.db database file in the root of your project, create a table we'll use for our webapp, and add one record to start us off with:

user=> (require '[next.jdbc :as jdbc] '[next.jdbc.sql :as sql])
nil
;; a hash map that describes the database we plan to use:
user=> (def db-spec {:dbtype "h2" :dbname "./my-db"})
#'user/db-spec
;; execute a single statement to create the locations table:
user=> (jdbc/execute-one! db-spec ["
CREATE TABLE locations (
  id bigint primary key auto_increment,
  x  integer,
  y  integer
)
"])
#:next.jdbc{:update-count 0}
;; insert a single row of data into that table:
user=> (sql/insert! db-spec :locations {:x 8 :y 9})
#:LOCATIONS{:ID 1} ; the generated key(s) from the insert
user=>

and press ctrl-d to exit.

You'll see that a file called my-db.mv.db has been created: this contains your my-db database.

Note: the #:namespace{:key value} notation is shorthand for {:namespace/key value} and is something you'll see a lot in Clojure. Namespace-qualified keys provide additional context: in the first case above :next.jdbc/update-count is produced by next.jdbc itself whereas :LOCATIONS/ID indicates the table and column name of the auto-increment key from the database.

For more about how to use the database functions, see the Getting Started with next.jdbc.

Create some db access functions

We're going to work bottom-up, so that our code is always in a state where we can evaluate it and try it out via the REPL (hopefully, via your REPL-connected editor).

Create a src/my_webapp/db.clj file and make it look like:

;; src/my_webapp/db.clj
(ns my-webapp.db
  (:require [next.jdbc.sql :as sql]))

(def db-spec {:dbtype "h2" :dbname "./my-db"})

(defn add-location-to-db
  [x y]
  (let [results (sql/insert! db-spec :locations {:x x :y y})]
    (assert (and (map? results) (:LOCATIONS/ID results)))
    results))

(defn get-xy
  [loc-id]
  (let [results (sql/query db-spec
                           ["select x, y from locations where id = ?" loc-id])]
    (assert (= (count results) 1))
    (first results)))

(defn get-all-locations
  []
  (sql/query db-spec ["select id, x, y from locations"]))

(comment
  (get-all-locations)
  ;; => [#:LOCATIONS{:ID 1, :X 8, :Y 9}]
  (get-xy 1)
  ;; => #:LOCATIONS{:X 8, :Y 9}
  )

Note that sql/query returns a vector of maps. Each map entry's key is a column name (as a Clojure keyword), and its value is the value for that column.

You can try the code out in the comment form by evaluating each expression in it, and you should see the same results as the inline comments show.

You can also try those calls yourself in a standalone REPL, if you like:

clj
Clojure 1.11.4
user=> (require 'my-webapp.db)
nil
;; you must require a namespace before you go into it:
user=> (in-ns 'my-webapp.db)
#object[clojure.lang.Namespace 0x707865bd "my-webapp.db"]
;; sql/query returns a vector:
my-webapp.db=> (sql/query db-spec
                 ["select x, y from locations where id = ?" 1])
[#:LOCATIONS{:X 8, :Y 9}]
;; the get-xy function only returns a single hash map:
my-webapp.db=> (get-xy 1)
#:LOCATIONS{:X 8, :Y 9}
my-webapp.db=>

Note: H2 uses UPPERCASE for its table and column names which looks a little ugly in Clojure. Most other databases will use lowercase for table and column names so you would get results like #:locations{:x 8, :y 9} so if you decide to change databases later on, as you evolve this web application, remember to change the case of keys in the views.clj file you'll create next.

Create your Views

Next, we will create the views, which generate our HTML pages.

Create a src/my_webapp/views.clj file and make it look like:

;; src/my_webapp/views.clj
(ns my-webapp.views
  (:require [hiccup.page :as page]
            [my-webapp.db :as db]
            [ring.util.anti-forgery :as util]))

(defn gen-page-head
  [title]
  [:head
   [:title (str "Locations: " title)]
   (page/include-css "/css/styles.css")])

(def header-links
  [:div#header-links
   "[ "
   [:a {:href "/"} "Home"]
   " | "
   [:a {:href "/add-location"} "Add a Location"]
   " | "
   [:a {:href "/all-locations"} "View All Locations"]
   " ]"])

(defn home-page
  []
  (page/html5
   (gen-page-head "Home")
   header-links
   [:h1 "Home"]
   [:p "Webapp to store and display some 2D (x,y) locations."]))

(defn add-location-page
  []
  (page/html5
   (gen-page-head "Add a Location")
   header-links
   [:h1 "Add a Location"]
   [:form {:action "/add-location" :method "POST"}
    (util/anti-forgery-field) ; prevents cross-site scripting attacks
    [:p "x value: " [:input {:type "text" :name "x"}]]
    [:p "y value: " [:input {:type "text" :name "y"}]]
    [:p [:input {:type "submit" :value "submit location"}]]]))

(defn add-location-results-page
  [{:keys [x y]}]
  (let [{id :LOCATIONS/ID} (db/add-location-to-db x y)]
    (page/html5
     (gen-page-head "Added a Location")
     header-links
     [:h1 "Added a Location"]
     [:p "Added [" x ", " y "] (id: " id ") to the db. "
      [:a {:href (str "/location/" id)} "See for yourself"]
      "."])))

(defn location-page
  [loc-id]
  (let [{x :LOCATIONS/X y :LOCATIONS/Y} (db/get-xy loc-id)]
    (page/html5
     (gen-page-head (str "Location " loc-id))
     header-links
     [:h1 "A Single Location"]
     [:p "id: " loc-id]
     [:p "x: " x]
     [:p "y: " y])))

(defn all-locations-page
  []
  (let [all-locs (db/get-all-locations)]
    (page/html5
     (gen-page-head "All Locations in the db")
     header-links
     [:h1 "All Locations"]
     [:table
      [:tr [:th "id"] [:th "x"] [:th "y"]]
      (for [loc all-locs]
        [:tr
         [:td (:LOCATIONS/ID loc)]
         [:td (:LOCATIONS/X loc)]
         [:td (:LOCATIONS/Y loc)]])])))

These functions generate all the HTML pages needed by our application.

Each of the functions with names ending in "-page" (which will be the ones being called from handler.clj in the next section) is returning just a string consisting of HTML markup. Compojure will take care of placing that into a response hashmap for us.

We use the {sym :key} form of destructuring in several functions to give local symbol names to the values associated with the database table/column keys.

Set up your routes

Finally, we're going to add the extra routes we need into the main file of our application, so that they call our new view functions.

In the basic src/my_webapp/handler.clj file you've created, we specify our webapp's routes inside the defroutes macro. That is, we assign a function to handle each of the url paths we'd like to support, and then at the end provide a "not found" page for any other url paths.

Make your handler.clj file look like this:

;; src/my_webapp/handler.clj
(ns my-webapp.handler
  (:require [compojure.core :refer [defroutes GET POST]] ; add POST here
            [compojure.route :as route]
            [my-webapp.views :as views] ; add this require
            [ring.adapter.jetty :as jetty]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults]]))

(defroutes app-routes ; replace the previous app-routes with this
  (GET "/"
    []
    (views/home-page))
  (GET "/add-location"
    []
    (views/add-location-page))
  (POST "/add-location"
    {params :params}
    (views/add-location-results-page params))
  (GET "/location/:loc-id"
    [loc-id]
    (views/location-page loc-id))
  (GET "/all-locations"
    []
    (views/all-locations-page))
  (route/resources "/")
  (route/not-found "Not Found"))

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

(defn -main []
  (jetty/run-jetty #'app {:port 3000}))

Each of those expressions in defroutes like (GET ...) or (POST ...) are so-called "routes". They each evaluate to a function that takes a Ring request hashmap and returns a response hashmap. Your views/foo function's job is to return that response hashmap, but note that Compojure is kind enough to make a suitable response map out of any HTML you return.

Incidentally, note the special destructuring that Compojure does for you in each of those routes. It can pull out url query (and body) parameters, as well as pieces of the url path requested, and hand them to your views functions. Read more about that at Compojure destructuring.

Run your webapp during development

Running from the command-line

You can run your webapp any time via clojure -M -m my-webapp.handler as shown above. Once it is running, visit http://localhost:3000 in your browser.

You should be able to stop the webapp by hitting ctrl-c (ctrl-z on Windows).

Note: changes made to your files while the webapp is running from the command-line will not be reflected until you restart the webapp!

Running interactively (in the REPL)

You can also run your webapp interactively, i.e., in the REPL, which allows for changing functions while your webapp is running and seeing those changes immediately.

Add the following comment form after the -main function:

(comment
  ;; evaluate this def form to start the webapp via the REPL:
  ;; :join? false runs the web server in the background!
  (def server (jetty/run-jetty #'app {:port 3000 :join? false}))
  ;; evaluate this form to stop the webapp via the the REPL:
  (.stop server)
  )

Deploy your webapp

For the final step, we're going to build an "uberjar". This is a file that contains your code plus the Clojure runtime plus all the libraries your code depends on: it's a single, self-contained file that can be executed by java alone and can easily be deployed to servers or services to put your application on the web.

In order to produce this .jar file, we will rely on the official tools.build library, and add a build.clj file.

All of the steps will be shown here but you can read the tools.build guide for more details.

To make your webapp suitable for deployment, make the following changes:

Changes in deps.edn

In your dep.edn file add the following, after the :deps hash map:

 :aliases
 {;; Run with clj -T:build function-in-build
  :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.4"}}
          :ns-default build}}

The whole deps.edn file should now look like this:

{:paths ["src" "resources"]
 :deps {;; basic Ring and web server:
        ring/ring-core {:mvn/version "1.12.2"}
        ring/ring-jetty-adapter {:mvn/version "1.12.2"}
        ;; logging, required by jetty:
        org.slf4j/slf4j-simple {:mvn/version "2.0.13"}

        ;; routing:
        compojure/compojure {:mvn/version "1.7.1"}

        ;; convenient package of "default" middleware:
        ring/ring-defaults {:mvn/version "0.5.0"}

        ;; to generate HTML:
        hiccup/hiccup {:mvn/version "1.0.5"}

        ;; for the database:
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}
        com.h2database/h2 {:mvn/version "2.2.224"}}
 :aliases
 {;; Run with clj -T:build function-in-build
  :build {:deps {io.github.clojure/tools.build {:mvn/version "0.10.4"}}
          :ns-default build}}}

Add a build.clj file

The tools.build library is intended to be used with a build.clj script which typically lives in the root of your project and is invoked via the :build alias in your project. It is a Clojure namespace, containing any number of functions that you can invoke using clojure -T:build and the function name.

The :ns-default key in the :build alias is typically set to build so that you can say clojure -T:build foo and the CLI will treat that as an invocation of the function build/foo. All such functions take a single argument, which is a hash map of arguments supplied on the command-line using Clojure-style syntax:

clojure -T:build foo :bar 42
;; invokes (build/foo {:bar 42})

For the purposes of this web application project, you want a single function that can build the "uberjar" you need. This is typically called uber so here is the build.clj file you need to add, alongside deps.edn at the top-level of your project:

(ns build
  (:require [clojure.tools.build.api :as b]))

;; the main namespace in your application:
(def main-ns 'my-webapp.handler)
;; where to compile your application:
(def class-dir "target/classes")
;; where to create the uberjar file:
(def uber-file "target/my-webapp.jar")

;; "basis" is a description of your project, as data, that includes
;; details about the paths and dependencies (libraries) it uses:
(def basis (b/create-basis {:project "deps.edn"}))

(defn clean [_]
  (b/delete {:path "target"}))

(defn uber [_]
  (clean nil)
  (b/copy-dir {:src-dirs ["src" "resources"]
               :target-dir class-dir})
  (b/compile-clj {:basis basis
                  :src-dirs ["src"]
                  :class-dir class-dir})
  (b/uber {:class-dir class-dir
           :uber-file uber-file
           :basis basis
           :main main-ns}))

Changes in handler.clj

In order to make it easier to invoke your application as an uberjar, we are going to make a couple of small changes.

First, we're going to add (:gen-class) to the end of the ns form at the top of the file so it looks like this:

(ns my-webapp.handler
  (:require [compojure.core :refer [defroutes GET POST]] ; add POST here
            [compojure.route :as route]
            [my-webapp.views :as views] ; add this require
            [ring.adapter.jetty :as jetty]
            [ring.middleware.defaults :refer [site-defaults wrap-defaults]])
  (:gen-class))

This tells Clojure to generate a JVM-compatible class for your main namespace so that the -main function can be invoked directly from Java instead of going through clojure.main as we've done so far with the Clojure CLI and the -M -m my-webapp.handler options.

Second, we're going to update the -main function so that you can specify the port on which to run the web application, so it isn't fixed to be 3000. We'll allow the port to specified either on the command-line, or as an environment variable, else default to a specific value (3000).

(defn -main [& [port]]
  ;; command-line arguments and environment variables are always
  ;; strings so we need to call parse-long on the result; which
  ;; means that if neither are specified and we provide the default,
  ;; then it has to be a string as well:
  (let [port (parse-long (or port
                             (System/getenv "PORT")
                             "3000"))]
    (jetty/run-jetty #'app {:port port})))

Build and Run it

Now create an uberjar of your webapp:

clojure -T:build uber

Note: the first time you run this command it will download all the libraries it needs for tools.build (quite a few libraries)!

And now you can run it directly:

java -jar target/my-webapp.jar 8080

(or on whatever port number you wish). If you run the JAR file from another folder, remember to copy the my-db.mv.db file to that folder! (or else it will create a new database file in that folder)

You could also run it like this (on macOS/Linux):

PORT=8000 java -jar target/my-webapp.jar

See Also

  • To get a head start with a more "batteries-included" project template, see Luminus.

Contributors

John Gabriele jmg3000@gmail.com (original author)

Ivan Kryvoruchko gildraug@gmail.com

Sean Corfield sean@corfield.org