m3tti / borkweb

🥇 babashka`s first fullstack clojure framework. That works with jvm clojure. ❗Batteries included❗
https://borkweb.org
MIT License
82 stars 4 forks source link

Borkweb

Slack Join Slack

Borkweb: A Simpler Approach to modern Web Development

Borkweb is an effort to create a full-stack Clojure framework that is simple, minimal and traditional. I believe that the complexity and overhead of modern web development frameworks have made it difficult for developers to focus on building great applications. Our goal is to provide a tool that allows developers to work efficiently and effectively, without getting bogged down in unnecessary configurations and integrations.

Why Borkweb?

I didn't create Borkweb as a proof-of-concept or a side project. I created it because I needed it. I was building real applications and was frustrated with the complexity and overhead of existing frameworks. I wanted a tool that would allow me to focus on building great applications, without getting bogged down in unnecessary configurations and integrations. Borkweb is the result of my efforts, and I've been using it in production for my own applications.

Core Values

Full-Stack Clojure

Borkweb is a full-stack framework that uses Clojure on all ends. This means you can write your frontend code in ClojureScript using Squint, your CSS in Gaka, and your backend code in Babashka. Everything backed by a traditional SQL database.

No Overhead

One of the key benefits of Borkweb is that it requires minimal overhead. You don't need to worry about setting up a complex build pipeline or managing a multitude of dependencies. With Babashka, you can simply write your code and run it. No need for Node.js, no need for a separate frontend build process. Just write, run, and deploy.

Contributing

Borkweb is an open-source project, and I welcome contributions from anyone who is interested in helping to make it better. If you have an idea for a feature or a bug fix, please open an issue or submit a pull request.

Dependencies

Frontend third party

Get started

borkweb only needs babashka to get started. To run the template just do a bb -m core and you are good to go. If you want to have the login and register code up and running you should consider to create a database. Borkweb comes with HSQLDB by default. So there is no need to have a real database at hand. It is recommended for production workloads to use Postgres or other RDBMS systems. The HSQLDB is set to support the Postgres dialect to make it seamless to switch between development and production without the need for a RDBMS system on your dev host.

Everything starts with a Database

The initialization of the database is currently done with an initsql.edn file which you can trigger with bb -m database.core/initialize-db. The database connection parameters are available in database/core.cljs just replace as you like. Currently hsqldb and postgres are used for the database backend but you can basically use any sql database that you like and which is supported by your runtime. If you want to use a diffrent db like datalevin you'll lose the registration and login functionality and have to adjust some stuff. The initsql.edn is basically honeysql. You can lookup all special keywords and such at the documentation.

Routing

Routing can be done in the routes.clj file found in the base folder. There are already premade helper functions to generate routes in a compojuresque style.

(route path method (fn [req] body))
(get path (fn [req] body))
(post path (fn [req] body))
(option path (fn [req] body))
(put path (fn [req] body))
(delete path (fn [req] body))

CRUD Helper and Database simplifications

Borkweb tries to not stop you in your creativity and implementations. But sometimes you just want to get stuff done without thinking up front how to implement stuff and how it should look like. Sometimes you just need some CRUD tool fast out of the door. This is where Borkweb's CRUD helper functions come handy. Currently we have 2 places for some of those helpers

CRUD Examples

(require [utils.crud :as crud])

(defn update
  [req]
  (crud/update!
   :req req
   :update-fn job/insert!
   :normalized-data (normalize-job-data req)
   :redirect-path "/job"))

(defn create
  [req]
  (crud/create!
   :req req
   :create-fn job/insert!
   :normalized-data (normalize-job-data req)
   ;; :does-already-exist? (some-check req) ;; optional
   :redirect-path "/job"))

(defn create&update
  [req]
  (crud/upsert!
   :req req
   :update-fn job/update!
   :create-fn job/insert!
   :normalized-data (normalize-job-data req)
   ;; :does-already-exist? (some-check req) ;; optional
   :redirect-path "/job"))

(defn delete [req]
  (crud/delete!
   :req req
   :delete-fn job/delete!
   :redirect-path "/job"))

;; routes.clj
...
(post "/save" create&update)
(post "/delete" delete)
...

New / Edit view

An example of how to use the create-update-form helper that generates a post form with the given inputs. Furthermore the example uses another helper out of the view/layout.clj that generates form-inputs with labels given by its type. You can extend it if you like. In this example we have added the trix editor with its own :type.

(crud/create-update-form
 :save-path "/post"
 :form-inputs
 (map l/form-input
  [{:type "hidden"
    :name "id"
    :value (:posts/id post)}
   {:label "Title"
    :type "input"
    :name "title"
    :value (:posts/title post)}
   {:label "Content"
    :type "trix"
    :name "content"
    :value (:posts/content post)}]))

Table View

Table View example with extra delete dialog button based on the modal feature present in borkweb.

[:div 
 (crud/table-view
  :new-path "/post/new"
  :elements (post/all-paged q page)
  :edit-path-fn #(str "/post/" (:posts/id %) "/edit")
  :actions-fn
  (fn [e]
   [:div
    [:a.text-danger {:href "#"
                     :data-bs-toggle "modal"
                     :data-bs-target (str "#" (:posts/id e) "-delete")} "Delete"]
    (l/modal
     :id (str (:posts/id e) "-delete")
     :title "Delete Post"
     :content [:div "Delete Post really?"]
     :actions
     [:div
      [:button {:type "button" :class "btn btn-secondary" :data-bs-dismiss "modal"} "Close"]
      [:form.ms-2.d-inline {:action "/post/delete" :method "post"}
       (c/csrf-token)
       [:input {:type "hidden" :name "id" :value (:posts/id e)}]
       [:input.btn.btn-danger {:type "submit" :data-bs-dismiss "modal" :value "Delete"}]]])]))
 (l/paginator req page (post/item-count) "/post")]

FileUpload

(require [view.layout :as l])
(require [utils.encode :as encode])

(defn some-handler
  [req]
  (l/layout
   req
   [:form {:action "/some/action" :method "post"}
    (l/form-input
     :id "file-upload" ;; is mandatory
     :label "My Upload"
     :name "file")
    [:input {:type "submit" :value "save"}]]))

(defn some-post-handler
  [req]
  (let [file (encode/decode->file (get-in req [:params "file"] "/some/file.pdf"))]
    ;; do something
    ))

CLJS

borkweb provides already everything you need to get started with cljs no need for any bundler or anything else. Get to resources/cljs drop your cljs code that is squint compliant and you are good to go. borkweb allready includes some examples for preact and preact web components. There are helper functions to compile cljs code in your hiccup templates. You can find them in view/components.clj cljs-module to generate js module code and cljs-resource to create plain javascript code. there is even ->js which can be used to trigger squint/cljs code inline.

;; resources/cljs/counter.cljs
(require '["https://esm.sh/preact@10.19.2" :as react])
(require '["https://esm.sh/preact@10.19.2/hooks" :as hooks])

(defn Counter []
  (let [[counter setCounter] (hooks/useState 0)]
    #jsx [:<>
          [:div "Counter " counter]
          [:div {:role "group" :class "btn-group"}
           [:button {:class "btn btn-primary" :onClick #(setCounter (inc counter))} "+"]
           [:button {:class "btn btn-primary" :onClick #(setCounter (dec counter))} "-"]]]))

(defonce el (js/document.getElementById "cljs"))

(react/render #jsx [Counter] el)
;; view/some_page.clj
(require '[view.components :as c])

(defn some-page [req]
  [:h1 "My fancy component page"]
  [:div#cljs]
  ;; just use the filename without cljs. the function will search in the resource/cljs folder.
  (c/cljs-module "counter"))

Roadmap

Articles