avatar image - a green paisley swirl bits all the way down
July 9, 2025
By: Nundrum

Clojure, Babashka, and Web CGI

  1. 》Why Bother?
  2. 》How About An Example?
  3. 》CGI?
  4. 》What Does The Script Look Like?
    1. 》》HTMX?
    2. 》》About That iconlist.clj?
  5. 》That’s It?

》Why Bother?

My house is filled with all kinds of weird projects. Even moreso my computers are filled with homegrown projects. I self-host a good few services. Many of the projects need data and sometimes that data needs a simple web interface.

Also, I like to make generative art. But more specifically I like taking sources of data and making them "artful." Some of that data has to be maintained, and image data is not always a great fit for the command line.

》How About An Example?

Ok, so, you want an example. There’s a big screen on my study wall that shows me all sorts of data about my life. Part of that project shows icons that represent devices that show up on my network. Instead of hand-picking every icon for each device, the code fetches them from the Noun Project.

Even though the algorithm usually picks a good word, some choices need to be overridden. I thought a web page to show and manage the available icons would be handy, but did not want to write some constantly-running web service just adds overhead on my hosts for something that would only get occasional use.

Since I was already serving web pages from that host, the idea was to try to connect Apache HTTPD’s CGI capability with Babashka.

》CGI?

The Common Gateway Interface describes how a web server can fire off a sub-process and return the output from said process. In typical unixy fashion, a bunch of environment variables are set up to pass to the sub-process and data is piped via stdin and stdout.

This means you can write a response in just about any language. You could even do it in bash! Which is kind of a great way to play around and really understand what’s going on with CGI.

Setting that up with Apache is pretty easy, too. This is how I enabled hanlding for .clj files in my public_html directory for development:

file: /etc/apache2/conf-available/serve-clj.conf:

<Directory "/home/*/public_html">
    Options +ExecCGI
    AddHandler cgi-script .clj
</Directory>

Make sure the cgid_module is enabled by running a2enmod cgid and then enable the config with a2enconf serve-clj.

If that isn’t enough, consult the Apache docs.

》What Does The Script Look Like?

Since this is a Babashka-based project, perhaps the most important thing is to ensure the shebang line points to your bb. Something like #!/bin/env bb or #!/usr/local/bin/bb depending on how you installed it.

Then it’s handy to have some functions that deal with how CGI passes info in the environment and data from stdin. The namespace below handles basic parsing of the QUERY_STRING variable as well as reading any data POSTed to the page via stdin.

(ns common)

(defn query-params
  "reads and parses query parameters from the URL
  via CGI QUERY_STRING environment variable"
  []
  (if-not (empty? (System/getenv "QUERY_STRING"))
    (let [pairs (-> (System/getenv "QUERY_STRING")
                (clojure.string/split , #"&"))
          kvs  (map
                  #(apply hash-map (clojure.string/split % #"=" 2))
                  pairs)]
      (into {} kvs))
    {}))

(defn read-post-body
  "reads the POST body from stdin"
  []
  (try (let [body (slurp *in*)
             parts (clojure.string/split body #"&")
             pairs (mapv #(clojure.string/split % #"=") parts)]
         (into {} pairs))
    (catch Exception e {})))

》》HTMX?

Now you’re asking "but what about the main script?!" In this case, everything is tied together with a main HTML page. Just a static page. Your normal, everyday index.html page. With the addition of HTMX! Explaining HTMX is way out of scope here, so let me just say HTMX is a JavaScript library that adds dynamic capabilites to normal HTML tags.

Adding HTMX to a page is easy - just one <script> tag:

<script src="https://unpkg.com/htmx.org@1.9.2"
integrity="sha384-L6OqL9pRWyyFU3+/
bjdSri+iIphTN/bvYyM37tICVyOJkWZLpP2vGn6VUEXgzg6h"
crossorigin="anonymous"></script>

Now imagine I have an iconlist.clj which provides the list of icons saved on the filesystem. HTMX allows for triggering it’s execution at load time. The output HTML from my script replaces the HTML element in the page. It looks like this:

<!-- this becomes a GET to /iconlist.clj at page load time -->
<!-- returned HTML is inserted in place of the div tag -->
<div id="iconlist" hx-get="iconlist.clj" hx-trigger="load">
loading icons...</div>

》》About That iconlist.clj?

This is the fun Clojury bit. It relies on Hiccup to simplify generating HTML as well as the common namespace defined above. Babashka’s fs lib handles listing files and working with paths.

#!/bin/env bb
(ns iconlist)
(require '[hiccup.core :as h])
(require '[babashka.fs :as fs])
(require '[common :as c])

(defn get-icon-info [path]
  (let [icondirs (filter fs/directory? (fs/list-dir path))]
    (for [dir icondirs
            :let [iconname (fs/file-name dir)
                  imgs (mapv str (fs/list-dir dir))]]
      {iconname imgs})))

(defn fix-path [p]
  (clojure.string/replace
    (str p)
    #"^/home/.*/public_html/iconkeep/" ""))

(defn show-icons [iconlist]
  (for [icon iconlist]
    (h/html [:img {:class "icon" :src (fix-path icon)
      :width 200 :height 200}])))

(defn get-selector []
  (if (= (System/getenv "REQUEST_METHOD") "GET")
                 #".*"
                 (re-pattern (str (get (c/read-post-body) "search")
		 ".*"))))

(println "Content-type: text/html\n")
(let [params   (c/query-params)
      selector (get-selector)
      root     (System/getenv "CONTEXT_DOCUMENT_ROOT")
      iconbase (str root "/iconkeep/iconbase")
      iconinfo (into {} (get-icon-info iconbase)) ]
  (doseq [[iconname icons] iconinfo]
    (if (re-matches selector iconname)
      (println (h/html
        [:div {:class "icongroup"}
	  [:span iconname] [:span (show-icons icons)] ])))))

Notice the (println "Content-type: text/html\n") line - CGI scripts have to return HTTP headers and body. Which is printed to stdout by the script, but returned as the HTTP response to the browser.

After that, it just loops through icons it finds in the filesystem and prints out a series of div, span, and img tags using Hiccup. Here’s a sample of what the client sees, but reformatted for easy reading:

<div class="icongroup">
 <span>ultraviolet</span>
 <span>
  <img class="icon" height="200"
  src="iconbase/ultraviolet/4604823-200.png" width="200" />
  <img class="icon" height="200"
  src="iconbase/ultraviolet/2148936-200.png" width="200" />
  <img class="icon" height="200"
  src="iconbase/ultraviolet/4431984-200.png" width="200" />
  <img class="icon" height="200"
  src="iconbase/ultraviolet/4431253-200.png" width="200" />
  <img class="icon" height="200"
  src="iconbase/ultraviolet/1894546-200.png" width="200" />
 </span>
</div>

As you can see, that’s pretty simple. To explain a few other parts:

The common namespace reads CONTEXT_DOCUMENT_ROOT from the environment, which the Apache CGI module provides. That lets the script "discover" the place it resides in the filesystem without hardcoding a path.

The selector is just a regex for matching which icons to return. HTMX allows forms to POST form data, and the common/read-post-body function parses that data. When the HTTP method is GET a wildcard regex is returned. When it’s POST the search field from the form is used to construct a regex.

All the rest is mainly about listing the files and mangling the path.

》That’s It?

Really, that’s all. No big backend frameworks. Only an http server. Nothing to occupy memory or CPU cycles until it’s needed.

Go forth and write your lightweight Clojure web apps!

End-of-Transmission

Tags: clojure babashka selfhosting
The System Wayfinder »

A blog by Nundrum

Links

  • @Nundrum on Mastodon
  • Archives
  • RSS
  • Clojurians Slack
  • Contacting Nundrum
  • P.E.P.R. Jacket Project

Recent Posts

  • Clojure, Babashka, and Web CGI
  • The System Wayfinder
  • Fountainvoid

Tags

  • re-frame
  • clojure
  • svg
  • async
  • interop
  • jna
  • cli
  • screensaver
  • platformio
  • font
  • babashka
  • utility
  • pepr jacket project
  • cyberpunk
  • clojurescript
  • selfhosting

Copyright © 2025 Nundrum

Powered by Cryogen