Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

The best part about Babashka is that it's really batteries included nowadays. I had to make a little UI to display some stats about an app at work, and decided to try using it with HTMX. Turned out to be a really good experience. Babashka has pretty much everything you need for a basic web app baked in, and HTMX lets you do dynamic loading on the page without having to bother with a Js frontend.

Best part is that bb can start nREPL with `bb --nrepl-server` and then you can connect an editor like Calva to it and develop the script interactively. Definitely recommend checking it out if you need to make a simple web UI. Here's an example of a full fledged web app:

    #!/usr/bin/env bb
    (require
     '[clojure.string :as str]
     '[org.httpkit.server :as srv]
     '[hiccup2.core :as hp]
     '[cheshire.core :as json]
     '[babashka.pods :as pods]
     '[clojure.java.io :as io]
     '[clojure.edn :as edn])
    (import '[java.net URLDecoder])

    (pods/load-pod 'org.babashka/postgresql "0.1.0")
    (require '[pod.babashka.postgresql :as pg])

    (defonce server (atom nil))
    (defonce conn (atom nil))

    (def favicon "data:image/x-icon;base64,AAABAAEAEBAAAAAAAABoBQAAFgAAACgAAAAQAAAAIAAAAAEACAAAAAAAAAEAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP//AAD8fwAA/H8AAPxjAAD/4wAA/+MAAMY/AADGPwAAxjEAAP/xAAD/8QAA4x8AAOMfAADjHwAA//8AAP//AAA=")

    (defn list-accounts [{:keys [from to]}]
      (pg/execute! @conn
                   ["select account_id, created_at
                     from accounts
                     where created_at between to_date(?, 'yyyy-mm-dd') and to_date(?, 'yyyy-mm-dd')"
                    from to]))

    (defn list-all-accounts [_req]
      (json/encode {:accounts (pg/execute! @conn ["select account_id, created_at from accounts"])}))

    (defn parse-body [{:keys [body]}]
      (reduce
       (fn [params param]
         (let [[k v] (str/split param #"=")]
           (assoc params (keyword k) (URLDecoder/decode v))))
       {}
       (-> body slurp (str/split #"&"))))

    (defn render [html]
      (str (hp/html html)))

    (defn render-accounts [request]
      (let [params (parse-body request)
            accounts (list-accounts params)]
        [:table.table {:id "accounts"}
         [:thead
          [:tr [:th "account id"] [:th "created at"]]]
         [:tbody
          (for [{:accounts/keys [account_id created_at]} accounts]
            [:tr [:td account_id] [:td (str created_at)]])]]))

    (defn date-str [date]
      (let [fmt (java.text.SimpleDateFormat. "yyyy-MM-dd")]
        (.format fmt date)))

    (defn account-stats []
      [:section.hero
       [:div.hero-body
        [:div.container
         [:div.columns
          [:div.column
           [:form.box
            {:hx-post "/accounts-in-range"
             :hx-target "#accounts"
             :hx-swap "outerHTML"}
            [:h1.title "Accounts"]
            [:div.field
             [:label.label {:for "from"} [:b "from "]]
             [:input.control {:type "date" :id "from" :name "from" :value (date-str (java.util.Date.))}]]
    
            [:div.field
             [:label.label {:for "to"} [:b " to "]]
             [:input.control {:type "date" :id "to" :name "to" :value (date-str (java.util.Date.))}]]
    
            [:button.button {:type "submit"} "list accounts"]]
           [:div.box [:table.table {:id "accounts"}]]]]]]])
    
    (defn home-page [_req]
      (render
       [:html
        [:head
         [:link {:href favicon :rel "icon" :type "image/x-icon"}]
         [:meta {:charset "UTF-8"}]
         [:title "Account Stats"]
         [:link {:href "https://cdn.jsdelivr.net/npm/bulma@1.0.0/css/bulma.min.css" :rel "stylesheet"}]
         [:link {:href "https://unpkg.com/todomvc-app-css@2.4.1/index.css" :rel "stylesheet"}]
         [:script {:src "https://unpkg.com/htmx.org@1.5.0/dist/htmx.min.js" :defer true}]
         [:script {:src "https://unpkg.com/hyperscript.org@0.8.1/dist/_hyperscript.min.js" :defer true}]]
        [:body
         (account-stats)]]))

    (defn handler [{:keys [uri request-method] :as req}]
      (condp = [request-method uri]
        [:get "/"]
        {:body (home-page req)
         :headers {"Content-Type" "text/html charset=utf-8"}
         :status 200}

        [:get "/accounts.json"]
        {:body (list-all-accounts req)
         :headers {"Content-Type" "application/json; charset=utf-8"}
         :status 200}

        [:post "/accounts-in-range"]
        {:body (render (render-accounts req))
         :status 200}

        {:body (str "page " uri " not found")
         :status 404}))

    (defn read-config []
      (if (.exists (io/file "config.edn"))
        (edn/read-string (slurp "config.edn"))
        {:port 3001
         :db {:dbtype   "postgresql"
              :host     "localhost"
              :dbname   "postgres"
              :user     "postgres"
              :password "postgres"
              :port     5432}}))
    
    (defn run []
      (let [{:keys [port db]} (read-config)]
        (reset! conn db)
        (when-let [server @server]
          (server))
        (reset! server
                (srv/run-server #(handler %) {:port port}))
        (println "started on port:" port)))

    ;; ensures process doesn't exit when running from command line
    (when (= "start" (first *command-line-args*))
      (run)
      @(promise))

    (comment
      ;; restart server 
      (do
        (when-let [instance @server] (instance))
        (reset! server nil)
        (run)))


I agree. It's a breath of fresh air in the Clojure world. I'm grateful to thoughtful builders like yourself and borkdude for bringing the language to new heights.


Babashka is definitely the most exciting thing currently happening in Clojure world in my opinion. And thanks, always great to hear my stuff ends up being useful. :)




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: