Skip to content

Latest commit

 

History

History
781 lines (641 loc) · 26.2 KB

clojure-web-app-workshop-functional-conf-2025.org

File metadata and controls

781 lines (641 loc) · 26.2 KB

Composing (Clojure) Web Stacks using Functional First Principles

Final reveal.js deck here:

1 init!

Clojure-icon.png

1.1 Statutory Health Warning!

lisplogo_warning2_256.png

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.jpg

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-clojurian.jpg 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

./professional-clojurian-office.jpg

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.

./lisplogo_alien_256.png

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