HTML with Hiccup & Integrant
Follow a complete HTML application from configuration through components, routes and rendered views.
Build and run
Requires Java 21+, Clojure CLI and the runtime named below. The example pins the compiler that supports native ^:async and await. Run these commands in a separate terminal from other examples.
git clone https://github.com/OUTCASTGEEK-TECH/fast-twitch.git
cd fast-twitch
git checkout a4eb73f96836d034c7a24fe09dc7d51e4c581f64
cd examples/02-hiccup-integrant-bulma
npm install
bb run-node
Request it
curl -i http://127.0.0.1:6464/
curl -i http://127.0.0.1:6464/hello/Ada
curl -i http://127.0.0.1:6464/missing
The first two requests return HTML with status 200. The third renders the application's missing-page view with status 404. /hello/Ada passes "Ada" to views/hello-page through :path-params.
Follow the request
configdeclares settings andig/refdependencies.ig/initstarts the graph in dependency order.::settingsreturns the settings map.::pagescloses over it and returns three handler functions.::routesproduces an ordinary route vector.::apppasses that vector and the missing-page handler torouting/routes.routing/ft-handleradapts the Ring handler to Fetch.runstarts the runtime on port 6464.html-responserenders Hiccup throughreplicant.string/render, producing an HTML string in a normal response map.
Read server.cljs and then views.cljs below. The views file defines the page structure; the server file connects that structure to requests. Bulma is a linked stylesheet, not part of Fast-Twitch's routing API.
Change it yourself
Add a :goodbye handler to ::pages and a /goodbye/:name entry to ::routes. Render the name through a Hiccup text node. Rebuild, request /goodbye/Ada, and confirm that a different name changes the page without changing the route definition.
Lifecycle and other runtimes
bb run-bun and bb run-deno compile and run the same app. bb deno-serve builds a separate Fetch export without CLI startup. PORT=7777 bb run-node changes the port. The example's shutdown stops the server; if you add resources to Integrant components, implement corresponding halt methods rather than relying on page functions to release them.
HTML composition guide · Routing API.
Complete source and build files
The files below are copied without abbreviation from Fast-Twitch revision a4eb73f96836d034c7a24fe09dc7d51e4c581f64. The walkthrough and API reference use the same revision.
Source: examples/02-hiccup-integrant-bulma/src/hiccupapp/server.cljs
View this exact file on GitHub.
(ns hiccupapp.server
[:require-macros [fast-twitch.macros :refer [env-var]]]
[:require [fast-twitch.routing :as routing :refer [start-server! stop-server!]]
[integrant.core :as ig]
[replicant.string :as html]
[hiccupapp.views :as views]]
[:refer-global :only
[Number console globalThis]])
(def request-options
{:protocol "HTTP/1.1" :remote-addr "127.0.0.1"})
(defn html-response
[page]
{:status 200
:headers {"Content-Type" "text/html; charset=utf-8"}
:body (html/render page)})
(defn not-found-page
[settings uri]
(assoc (html-response (views/not-found-page settings uri)) :status 404))
(defmethod ig/init-key ::settings
[_ options]
options)
(defmethod ig/init-key ::pages
[_ {:keys [settings]}]
{:home (fn [_request]
(html-response (views/home-page settings)))
:hello (fn [{:keys [path-params]}]
(html-response (views/hello-page settings (:name path-params))))
:not-found (fn [{:keys [uri]}]
(not-found-page settings uri))})
(defmethod ig/init-key ::routes
[_ {:keys [pages]}]
[{:pattern "/" :method :get :handler (:home pages)}
{:pattern "/hello/:name" :method :get :handler (:hello pages)}])
(defmethod ig/init-key ::app
[_ {:keys [routes pages]}]
(routing/routes routes (:not-found pages)))
(def config
{::settings {:title "Hiccup through Integrant"
:bulma-css "https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css"}
::pages {:settings (ig/ref ::settings)}
::routes {:pages (ig/ref ::pages)}
::app {:routes (ig/ref ::routes) :pages (ig/ref ::pages)}})
(defonce system
(ig/init config))
(def handler
(routing/ft-handler (::app system) request-options))
(defn shutdown
[]
(stop-server! :force true
:callback (fn []
(console.log "Server stopped"))))
(defn run
[]
(start-server! handler
{:port (Number (or (env-var "PORT") 6464))
:hostname (or (env-var "HOSTNAME") "127.0.0.1")
:onListen (fn [addr]
(console/log
(str "Hiccup Integrant server listening at http://"
(aget addr "hostname")
":" (aget addr "port"))))}))
(set! *main-cli-fn* run)
(defn ^:export fetch
[request]
(handler request))
Source: examples/02-hiccup-integrant-bulma/src/hiccupapp/views.cljs
View this exact file on GitHub.
(ns hiccupapp.views)
(defn layout
[{:keys [title bulma-css]} & body]
[:html
[:head [:meta {:charset "utf-8"}]
[:meta {:name "viewport" :content "width=device-width, initial-scale=1"}]
[:title title] [:link {:rel "stylesheet" :href bulma-css}]]
[:body
[:section.hero.is-link
[:div.hero-body
[:div.container [:p.subtitle "fast-twitch newbie example"] [:h1.title title]]]]
[:main.section [:div.container body]]]])
(defn home-page
[settings]
(layout settings
[:div.columns
[:section.column [:h2.title.is-4 "Hiccup is data"]
[:div.box [:p "This box came from a vector in ClojureScript source code."]
[:pre "[:div.box\n [:p \"This box came from a vector.\"]]"]]]
[:section.column [:h2.title.is-4 "Integrant wires the parts"]
[:div.box
[:p "The config map builds settings, page handlers, routes, and the app."]
[:a.button.is-link {:href "/hello/Newbie"} "Try /hello/Newbie"]]]]))
(defn hello-page
[settings name]
(layout settings
[:div.box [:h2.title.is-3 (str "Hello, " name)]
[:p "The name came from a path parameter in the request map."]
[:p [:a {:href "/"} "Back home"]]]))
(defn not-found-page
[settings uri]
(layout settings
[:div.notification.is-warning [:strong "No route matched "] [:code uri]]))
Source: examples/02-hiccup-integrant-bulma/deps.edn
View this exact file on GitHub.
{:aliases {:nREPL {:extra-deps {cider/cider-nrepl {:mvn/version "0.62.2"}
cider/piggieback {:mvn/version "0.7.0"}
nrepl/nrepl {:mvn/version "1.7.0"}}
:main-opts
["-m" "nrepl.cmdline" "--middleware"
"[cider.nrepl/cider-middleware, cider.piggieback/wrap-cljs-repl]"]}
:test {:extra-paths ["test"]}}
:deps {fast-twitch/fast-twitch {:local/root "../.."}
integrant/integrant {:mvn/version "1.0.1"}
no.cjohansen/replicant {:mvn/version "2026.07.1"}
org.clojure/clojurescript {:git/sha "56a94ac13bfac22108e4d868dfd4763edaceb864"
:git/url "https://github.com/clojure/clojurescript"}}
:paths ["src"]}
;; Start the example nREPL server
;; clj -M:nREPL
Source: examples/02-hiccup-integrant-bulma/app.edn
View this exact file on GitHub.
{:aot-cache true
:cache-analysis true
:dump-core true
:fn-invoke-direct true
:infer-externs :all
:language-in :es-next
:language-out :es-next
:main hiccupapp.server
:optimizations :simple
:optimize-constants true
:output-dir "build"
:output-to "server.cjs"
:output-wrapper "%s\nexports.fetch = hiccupapp.server.fetch;\n"
:parallel-build true
:pretty-print true
:source-map "server.cjs.map"
:stable-names true
:static-fns true
:target :nodejs}
Source: examples/02-hiccup-integrant-bulma/bb.edn
View this exact file on GitHub.
{:tasks {build {:doc "Compile ClojureScript to server.cjs"
:task (shell "clj" "-M" "-m" "cljs.main" "-co" "app.edn" "-c")}
deno-serve {:doc "Build and serve the exported Fetch handler with Deno"
:task (let [port (or (System/getenv "PORT") "6464")
jobs (or (System/getenv "DENO_JOBS") "1")]
(shell "clj"
"-M"
"-m"
"cljs.main"
"-co"
"app.edn"
"-co"
(pr-str {:nodejs-rt false
:output-dir "build/serve"
:output-to "build/server-serve.cjs"
:source-map "build/server-serve.cjs.map"})
"-c")
(shell {:extra-env {"DENO_JOBS" jobs "PORT" port}}
"deno"
"serve"
"--parallel"
"-A"
"--port"
port
"build/server-serve.cjs"))}
run-bun {:depends [build]
:doc "Build and run server.cjs with Bun"
:task (shell "bun server.cjs")}
run-deno {:depends [build]
:doc "Build and run server.cjs with Deno"
:task (shell "deno run -A server.cjs")}
run-node {:depends [build]
:doc "Build and run server.cjs with Node"
:task (shell "node server.cjs")}}}
Source: examples/02-hiccup-integrant-bulma/package.json
View this exact file on GitHub.
{
"name": "fast-twitch-03-hiccup-integrant-bulma",
"private": true,
"type": "commonjs",
"scripts": {
"build": "bb build",
"node": "bb run-node",
"deno": "bb run-deno",
"bun": "bb run-bun"
},
"devDependencies": {
"@cljs-oss/module-deps": "^1.1.1"
}
}
Source: examples/02-hiccup-integrant-bulma/deno.jsonc
View this exact file on GitHub.
{
"tasks": {
"build": "bb build",
"start": "bb run-deno",
"serve": "bb deno-serve"
}
}