Composing (Clojure) Web Stacks using Functional First Principles

Functional Conf, 2025

Aditya Athalye

evalapply.org
(about, contact, hire)

2025-01-24

1. init!

Clojure logo.

1.1. Statutory Health Warning!

Warning! Built with Lisp.

1.2. Approach ๐Ÿ›ฌ

  • Real talk: My actual beliefs + code ๐Ÿซ€๐Ÿค‘
  • No magic: All workshop sources open ๐Ÿซฐ๐Ÿ„ฏ
  • Adult content: Simple != Toy โ˜ฏ โง“
  • Law of Mouse Pointer ๐Ÿญ๐Ÿ‘‰
    • Not for you? Click out!
  • Async., FTW! ๐Ÿ’ฌ๐Ÿ’ฌ๐Ÿ’ฌ๐Ÿ’ฌ๐Ÿ’ฌ
    • All the Qs! Puns! Hot takes! โœ…
    • Have fn! ๐Ÿฅณ
    • Keep it classy! ๐Ÿ˜Ž

2. Session breakdown

"Breakdown - Kurt Russell. It could happen to you."

2.1. Official Session (90 mins)

  • I Drive (~20 min)
    • fast-paced
    • scripted live demo
    • feel free to discuss/ask in chat
  • You Drive (~60 min)
    • you-paced
    • free-for-all hallway track
    • make me struggle to live code answers

2.2. Unofficial Unsession ($TillTheyKickUsOut mins)

  • GOTO Hangout Table
    • Live Video
    • MORE Hallway Track!

2.3. Chat nonstop!

Optionally, share:

  • name / nick
    • Aditya / Adi (he/him)
  • title / org
    • C(E/A)O / evalapply.org
  • Web Dev. (Y/N)? Y
    • For โ™ฅ: Clojure (+ libs, SQLite, HTMX)
    • For $: Clojure (+ libs, SQLite, HTMX)
  • Do you even Cloje?

    user=> (map always-be-clojing? [โ™ฅ $])
    [โ™ฅ $] ; emoji if yes, else nil
    

3. GOTO Workshop Content

4. Demo

demo-lition-man.jpg

4.1. Focus…

  • Focus: primary intuition-building ๐Ÿง ๐Ÿ—ผ
    • ideas, names, behaviours, design sensibilities
    • general dev workflow (REPL DD)
  • De-focus: secondary details ๐Ÿคจ๐Ÿ”ŽโŒ
    • syntax, parentheses, types
    • specific IDE and/or dev tooling

4.2. Null Project: Demo Calling Convention

  • Create empty dependency file

    echo '{}' > "deps.edn"
    
  • Create namespace with no-op entry point

    cat > "app.clj" << 'EOF'
    (ns app
      (:gen-class))
    
    ;; FIXME, please. Make me do some work!
    (defn -main [])
    EOF
    
  • Invoke from cmd line

    $ ls
    deps.edn app.clj
    $ clojure -M -m app # does nothing
    

4.3. Catch-All Web App: Simple != Toy

  • Globally namespaced

    deps.edn
    src/org/evalapply/catchall_app.clj
    
  • Add basic definitions to deps.edn

    {:paths ["src"]
     :deps {org.clojure/clojure {:mvn/version "1.12.0"}
            ring/ring-jetty-adapter {:mvn/version "1.13.0"}}}
    
    • Add source dir to Java class path
    • Explicit Clojure dep. (deterministic builds)
    • Prod-grade clj adapter to Jetty Server

4.3.1. Add "echo" HTTP handler

  • To catchall_app.clj

    (ns org.evalapply.catchall-app
      (:require [ring.adapter.jetty :as jetty])
      (:gen-class))
    
    (defn echo-handler [request]
      {:status 200
       :headers {"Content-Type" "text/plain;charset=utf-8"}
       :body (pr-str request)})
    
    (defn run-jetty
      [port]
      (println "Starting jetty at: " port)
      (jetty/run-jetty echo-handler
                       {:port port :join? false}))
    
    (defn -main [& args]
      (printl "Calling -main with args: " args)
      (run-jetty 3000))
    

4.3.2. Run and try via the command line

  • Java-like edit-compile-run cycle

    clojure -M -m org.evalapply.catchall-app
    
    curl http://localhost:3000
    
    curl -XPOST http://localhost:3000/foobar
    
    curl http://localhost:3000/foobar?search=%22can%20you%20read%20me%22
    

4.3.3. REPL-Driven-Development

  • Run and mould one's software LIVE

    (comment ; "'Rich' comment form"
      ;; Inspect live object
      (do (require 'clojure.reflect)
          (clojure.reflect/reflect server))
    
      ;; Capture values to inspect them at will.
      (def responses (atom []))
    
      (defn capture-response
        [response]
        (swap! responses conj response))
    
      (add-tap capture-response)
    
      (tap> {:status 200 :body "hi"})
    
      ;; Try Out dependencies:
      ;; - Add lib for current REPL session,
      ;; - without modifying deps.edn file
      (require 'clojure.repl.deps)
    
      (clojure.repl.deps/add-lib
       'org.clojure/data.json {:mvn/version "2.5.1"})
    
      ;; Temporarily replace top-level defn
      (defn echo-handler
        "TODO:
          - copy down body of top-level defn
          - handle json=true query param
          - evaluate to replace definition"
        []))
    

4.4. Smol "Web 1.0" App

  • "It's Just Data and Functions"

    deps.edn
    build.clj
    src/org/evalapply/catchall_app.clj
    test/org/evalapply/catchall_app_test.clj
    
  • deps.clj: Add real-world HTTP middleware
  • build.clj: deps are data, builds are programs
  • src: make custom router
  • test: to avoid creating a scandal :)

4.5. Big(ger) Web App

Mix in Libraries as needed

{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.12.0"}

        ;; Ring HTTP utilities: https://github.com/ring-clojure/ring
        ring/ring-core {:mvn/version "1.12.2"}
        ring/ring-jetty-adapter {:mvn/version "1.12.2"} ; embedded Jetty
        ring-cors/ring-cors {:mvn/version "0.1.13"}

        ;; System composition and configuration: https://github.com/weavejester/integrant
        integrant/integrant {:mvn/version "0.13.0"} ; define/start/stop system
        aero/aero {:mvn/version "1.1.6"} ; EDN-file-based configuration, might not need it

        ;; HTTP Routing and coercion: https://github.com/metosin/reitit
        metosin/reitit-core {:mvn/version "0.7.2"} ; routing core
        metosin/reitit-ring {:mvn/version "0.7.2"} ; ring router
        metosin/reitit-middleware {:mvn/version "0.7.2"} ; common middleware
        metosin/reitit-malli {:mvn/version "0.7.2"} ; malli coercion

        ;; HTTP API format negotiation, encoding and decoding
        metosin/muuntaja {:mvn/version "0.6.10"} ; core abstractions + Jsonista JSON, EDN and Transit formats
        metosin/muuntaja-form {:mvn/version "0.6.10"} ; application/x-www-form-urlencoded formatter using ring-codec

        ;; Data Utilities
        metosin/malli {:mvn/version "0.16.4"} ; specify, validate, coerce data

        ;; Database Utilities
        com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"} ; JDBC adapter
        org.xerial/sqlite-jdbc {:mvn/version "3.46.1.0"} ; SQLite JDBC driver
        com.zaxxer/HikariCP {:mvn/version "6.0.0"} ; connection pooling

        ;; Web Frontend
        hiccup/hiccup {:mvn/version "2.0.0-RC3"} ; Server-rendered HTML as Clojure data

        ;; Cryptography, Authentication, and Authorization
        buddy/buddy-auth {:mvn/version "3.0.1"} ; authenticate, authorize
        ;; buddy/buddy-hashers {:mvn/version "2.0.167"} ; hashing utils
        ;; buddy/buddy-sign {:mvn/version "3.6.1-359"} ;  High level message signing library.

        ;; Time
        clojure.java-time/clojure.java-time {:mvn/version "1.4.2"}

        ;; Logging
        org.clojure/tools.logging {:mvn/version "1.3.0"}
        org.slf4j/slf4j-simple {:mvn/version "2.0.16"}}}

4.6. Frameworks, Starter Kits, Project Management Systems

Professional Developer.
courtesy: @lambduhh

4.6.1. Real-world Starter kits

4.6.2. Frameworks

  • Biff: Biff helps solo developers move fast.
  • Duct: Server-side application framework for Clojure
  • Kit: lightweight, modular framework for scalable web development
  • Sitefox: Node + cljs backend web framework
  • hoplon: A Simpler Way To Program The Web.
  • hyperfiddle: a spreadsheet for internal frontend development

4.6.3. Project Management Systems

  • polylith
    • One Source Tree, Many Distinct Apps
    • Polylith is a software architecture that applies
      functional thinking at the system scale.
      It helps us build simple, maintainable, testable,
      and scalable backend systems.
  • Choose Your Own Adventure

5. Roundup: Clojurish Software Design Maxims

A Clojuring hard at work on their Hammock.

5.1. "A": Always. "B": Be. "C": Composing.

rich-hickey-value-of-values.jpg always-be-closing.jpg
  • Immutable Generic Data
  • Pure Functions
  • Open-ended Polymorphism
  • Composable Libraries
  • Specifications and Contracts

5.2. "It's Just Data"

  • Describe domain in terms of data.
    • e.g. deps.edn, hiccup, aero, integrant etc.
  • Lift domain concepts out of functions.
    • e.g. routing (reitit, ataraxy), interceptors (pedestal, re-frame)
  • Stick domain concepts into var metadata.
    • e.g. clojure.test fixtures
  • Roll your own DSL as plain data.
    • honeysql (intermediate), datalog (advanced)
    • multimethods are just mini ad-hoc interpreters of "Data DSLs"

5.3. "Decomplect"

  • Find and pry apart tight couplings
    • deps, fn call chains, file->code mapping
  • Specifications and Contracts
    • clojure.spec, malli
  • "System" composition, at build / compile time:
    • Clean Dependency Injection
    • component, integrant, mount

5.4. "Rich Abstracts So You Don't Have To"

  • Generic functions over generic data
  • Mutually composable interfaces
  • Provide Machine tools instead of Factories
  • Completely opt-in ("a la carte")

5.5. "Simplicity" (ain't so easy.)

"It's simple to be happy, difficult to be simple." - Bawarchi
"Slow is smooth. Smooth is Fast." - The SEALs

6. Thanks!

Aditya Athalye
evalapply.org/hire

I help small B2B SaaS teams deploy writing culture as a 10x-ing strategy.

Lisp Alien.

(Also can Cloje up that Micro SaaS you want to build!)

7. Extras

7.1. lol

https://x.com/jordwalke/status/1720370436322857327

react-creator-vs-design-patterns.png

7.2. Solve The Expression Problem

Playtime:

  • What if we frame everything in terms of the Expression Problem?
  • Add a new Y, extend all Xs to it?
    • Without cooperation of existing Xs?
  • Add new X, extend all Ys to it?
    • Without cooperation of existing Ys?
| X * Y | y1 | y2 | y3 | ... |
|-------+----+----+----+-----|
| x1    |    |    |    |     |
| x2    |    |    |    |     |
| x3    |    |    |    |     |
| ...   |    |    |    |     |

7.2.1. Methods x Classes

  • Add a new Class, extend all Methods to it?
    • Without cooperation of existing Methods?
  • Add new Method, extend all Classes to it?
    • Without cooperation of existing Classes?
| Method * Class | FormSubmit | Dropdown | CheckList | ... |
|----------------+------------+----------+-----------+-----|
| click!         |            |          |           |     |
| exists?        |            |          |           |     |
| visible?       |            |          |           |     |
| select!        |            |          |           |     |
| deselect!      |            |          |           |     |
| ...            |            |          |           |     |

7.2.2. Functions x Types

  • Add a new Type, extend all Functions to it?
    • Without cooperation of existing Functions?
  • Add new Function, extend all Types to it?
    • Without cooperation of existing Types?
| fn * <T> | List | Array | Tuple | HashMap | ... |
|----------+------+-------+-------+---------+-----|
| map      |      |       |       |         |     |
| filter   |      |       |       |         |     |
| take     |      |       |       |         |     |
| drop     |      |       |       |         |     |
| ...      |      |       |       |         |     |

7.2.3. Data Structure x Domain Entity

Expression Problem?

| Data x Entity     | Person | Persons | Singles | Couples |
|-------------------+--------+---------+---------+---------|
| List[Pair[?,?]]   |        |         |         |         |
| Array[Tuple[?,?]] |        |         |         |         |
| HashMap[?,?]      |        |         |         |         |
| ...               |        |         |         |         |

7.2.4. HTTP verbs x HTTP requests

What if we frame in terms of Expression Problem?

| Verb * Request | /slug-1/ | /slug-2/ | /slug-3/ | ... |
|----------------+----------+----------+----------+-----|
| GET            |          |          |          |     |
| PUT            |          |          |          |     |
| POST           |          |          |          |     |
| DELETE         |          |          |          |     |
| PATCH          |          |          |          |     |

7.2.5. Apps: parts x projects

Expression Problem?

| Parts * Apps | app-1 | app-2 | service-1 | service-2 | ... |
|--------------+-------+-------+-----------+-----------+-----|
| primary DB   |       |       |           |           |     |
| cache        |       |       |           |           |     |
| queue        |       |       |           |           |     |
| web server   |       |       |           |           |     |
| settings     |       |       |           |           |     |

7.2.6. Infra: parts x use-cases

Expression Problem?

| Infra * Use | primary DB | cache | queue | search | ... |
|-------------+------------+-------+-------+--------+-----|
| File System |            |       |       |        |     |
| SQLite      |            |       |       |        |     |
| Postgres    |            |       |       |        |     |
| Redis       |            |       |       |        |     |
| Kafka       |            |       |       |        |     |

7.2.7. Web Protocols: Provider x Consumer

  • Why the whole web works
    • It's just plain data (e.g. HTTP)
    • Specified by a general specification
    • servers, clients don't need each
      others' permissions, or
      foreknowledge to implement
      and/or interoperate
| Server * Client | Chrome | FF | MobileApps | curl | ... |
|-----------------+--------+----+------------+------+-----|
| nginx           |        |    |            |      |     |
| node            |        |    |            |      |     |
| jetty           |        |    |            |      |     |
| hunchertoot     |        |    |            |      |     |
| caddy           |        |    |            |      |     |
| WSGI            |        |    |            |      |     |
| CGI             |        |    |            |      |     |
| Gunicorn        |        |    |            |      |     |
| etc...          |        |    |            |      |     |

7.2.8. General design principle

  • Think in terms of informal protocols
  • Defined in words
  • Expressed as plain data (HTTP, Hypertext)
  • Checked/enforced via independent validators

7.3. Show the transformations…

7.4. Steps

  1. Handle basic Ring-HTTP request using a function
    1. Echo the request back in the body (pr-str)
  2. Parse HTTP as generic data
    1. Use ring-jetty to get requests-as-maps
    2. Use
    3. Show "just data" responses as alternative
  3. Make a basic static router (case)
    1. Home -> Redirect
    2. ping -> Health check
    3. Hello -> (or name "World")
  4. Swap out our server
  5. Make an extensible polymorphic router (multimethods)
  6. Swap out router with Reitit
  7. Add settings, with :envs
  8. Add system, with :envs

7.5. Heart of a web app

HTTP Request -> app -> HTTP Response

7.6. Heart of a web app: expanded

HTTP Request -> app -> HTTP Response

HTTP request ->
  /pattern-1/ method-1
  /pattern-2/ method-2
  /pattern-3/ method-3
              -> HTTP response

7.7. Heart of a web app: expanded more

HTTP Request -> app -> HTTP Response

HTTP request ->
  GET    /uri-1/ getter-method
  PUT    /uri-1/ putter-method
  POST   /uri-1/ poster-method
  PATCH  /uri-1/ patcher-method
  DELETE /uri-1/ deleter-method
                 -> HTTP response

Polymorphic Dispatch Machine

7.8. HTTP Request -> app -> HTTP Response

curl -v https://www.evalapply.org/posts/hello-world/index.html

  • Request

    > GET /posts/hello-world/index.html HTTP/2
    > Host: www.evalapply.org
    > User-Agent: curl/8.5.0
    > Accept: */*
    >
    
  • Response

    < HTTP/2 200
    < date: Sun, 19 Jan 2025 13:00:58 GMT
    < content-type: text/html; charset=utf-8
    < last-modified: Tue, 07 Jan 2025 18:40:29 GMT
    < access-control-allow-origin: *
    

7.9. HTTP Request -> app -> HTTP Response (contd. 2)

  • Response (contd…)

    < expires: Sun, 19 Jan 2025 13:10:21 GMT
    < cache-control: max-age=600
    < x-proxy-cache: MISS
    < x-github-request-id: 30B3:11B9:1374016:13A8BA4:678CF765
    < via: 1.1 varnish
    < age: 0
    < x-served-by: cache-mrs10566-MRS
    < x-cache: HIT
    < x-cache-hits: 1
    < x-timer: S1737291658.048786,VS0,VE119
    < vary: Accept-Encoding
    

7.10. HTTP Request -> app -> HTTP Response (contd. 3)

  • Response (contd…)

    < x-fastly-request-id: 5acec03e79d6fd85384ab5d43d0d862a41261297
    < cf-cache-status: DYNAMIC
    < report-to: {"endpoints":[{"url":"https:\/\/a.nel.cloudflare.com\/report\/v4?s=ovUNa9WMW%2FtaMfQlrsj3fsvm9XCyTXlWOzIO5PNfH6TVZx4aoWGLJVyGNFBP4KdhlwJGsd%2FJ5q3c1x2zTX2rzEBVX0HQd27jZNAfXR%2FwavmW8XexYZ8RnSjr3a2ORaE66mFxQ%2FAKRGqDdYP73FBBBA%3D%3D"}],"group":"cf-nel","max_age":604800}
    < nel: {"success_fraction":0,"report_to":"cf-nel","max_age":604800}
    < server: cloudflare
    < cf-ray: 904702be4c31076a-MRS
    < alt-svc: h3=":443"; ma=86400
    < server-timing: cfL4;desc="?proto=TCP&rtt=173437&min_rtt=165923&rtt_var=60025&sent=8&recv=9&lost=0&retrans=0&sent_bytes=3416&recv_bytes=778&delivery_rate=18968&cwnd=254&unsent_bytes=0&cid=ad859fdfe8400633&ts=428&x=0"
    <
    

7.11. "It's Just Data" Philosophy

  • Generic
    • Clojure Data Structures {Any, Any}
  • Plaintext Data
    • Human and Machine readable
  • Specifications
    • Human-made, Machine-checked
    • The Ring Specification

7.11.1. Ring Spec: HTTP REQUEST as Generic Data

  • Plain HTTP request -> parse -> Plain Clojure data
{:scheme :http,
 :request-method :get,
 :uri "/foo/bar/baz",
 :headers {"accept" "*/*",
           "user-agent" "curl/7.81.0",
           "host" "localhost:3000"},
 :query-string "search=wassup%20world",
 :body #object[org.eclipse.jetty.server.HttpInput 0x2a91914a "HttpInput@714182986 cs=HttpChannelState@2eae00c0{s=HANDLING rs=BLOCKING os=OPEN is=IDLE awp=false se=false i=true al=0} cp=org.eclipse.jetty.server.BlockingContentProducer@6bac9b71 eof=false"]}

7.11.2. Ring Spec: HTTP RESPONSE as Generic Data

  • RESPONSE as plain Clojure data -> serialize -> Plain HTTP RESPONSE
{:status 200
 :headers {"Content-Type" "text/html;charset=UTF-8"}
 :body "<h1>optional</h1>"}

7.11.3. Hiccup: HTML as Plain Clojure Data

[:div {:class "wow-list"}
 [:ul (list
       [:li "such data"]
       [:li "much generic"]
       [:li "very html"]
       [:li "wow!"])]]