Javascript and Clojure code

Javascript and Clojure code.

Introduction

This blog post is a continuation for my five earlier blog posts, in which I implemented a simple Webstore application using Javascript / Typescript / React / Node / Express stack:

For this new blog post, I implemented the same Webstore demonstration, this time using Clojure / Clojurescript. This blog post describes the similarities and differences between the two technologies and ecosystems: Javascript vs. Clojure.

Both implementations can be found in my Github account:

Technology Summary

Both demonstrations are the same: a simple Webstore application, which allows users to browse product groups, and products. Both implementations have the similar backends providing the exact same API, and similar frontends, both using React and providing the exact same UI look-and-feel.

js-node-ts-react:

clojure-full-stack-demo:

About Comparison

I compare various aspects of these two ecosystems based on the experience when I implemented these two full-stack applications. So, this is a very personal comparison (read the disclaimer at the end).

Backend - Language

Which backend language gets the job done? Both are good languages. But personally, I don’t like Javascript’s single-threaded asynchronic programming model that much. The single-threaded environment uses the event loop which requires callbacks (nowadays you can use async/await).

Javascript is also as a language a bit ugly. Typescript makes Javascript programming definitely more enjoyable since it makes programming less error-prone.

This is very personal. But I just like Clojure a lot. The language is really well-designed. The standard library is excellent. You cannot have a powerful REPL unless you are using a Lisp.

Clojure example:

(defn get-product [pg-id p-id products]
  (first (filter (fn [item] (and (= (:pgId item) pg-id) (= (:pId item) p-id))) products)))

Javascript example:

async function getProduct(pgId, pId) {
  const productsKey = `pg-${pgId}-products`;
  let products = domain[productsKey];
  if ((products === null) || (products === undefined)) {
    await loadProducts(pgId);
    products = domain[productsKey];
  }
  const filtered = products.filter((row) => row.pId === pId && row.pgId === pgId);
  const product = filtered[0];
  return product;
}

Backend - Productivity

If you are a seasoned Javascript/Typescript programmer or Clojure programmer you can be just as productive writing your backend using Typescript or Clojure.

Backend - Runtime

Clojure uses JVM as runtime. JVM is battle-tested technology which provides a real multi-thread runtime.

Javascript/Typescript runs on Node on the backend side. Just one thread.

Backend - Tooling and Libraries

Javascript uses npm as the package manager. You can also use pnpm or yarn. Personally I find Javascript’s package management ecosystem a real mess.

Clojure uses deps.edn and e.g. Maven repository.

I have a feeling that the libraries in the Clojure side seem to be more stable.

Clojure example to use the libraries:

(ns backend.db.users
  (:require [clojure.tools.logging :as log]
            [buddy.sign.jwt :as buddy-jwt]
            [clojure.data.codec.base64 :as base64]
            [clj-time.core :as clj-time]))
...
(defn generate-token [env username]
  (log/debug (str "ENTER generate-token, username: " username))
  (let [my-secret my-hex-secret
        exp-time (clj-time/plus (clj-time/now) (clj-time/seconds (get-in env [:options :jwt :exp])))
        my-claim {:username username :exp exp-time}
        json-web-token (buddy-jwt/sign my-claim my-secret)]
    json-web-token))

Javascript example to use the libraries:

import jwt from 'jsonwebtoken';
import logger from '../util/logger.mjs';
import { ValidationError } from '../util/errors.mjs';
...
function generateToken(username) {
  return jwt.sign({ username }, SECRET, { expiresIn: EXPRIRES_IN });
}

Backend - Server and Routing

I used jetty as the web server with ring abstraction, and metosin/reitit as the routing library in the Clojure side.

I used express for both as the backend server and for the routing in the Javascript side.

Both ecosystems were quite straigthforward to use.

Clojure example:

    ["/product-groups" {:get {:summary "Get products groups"
                              :responses {200 {:description "Product groups success"}}
                              :parameters {:query [:map]}
                              :handler (fn [req]
                                         (let [token (get-in req [:headers "x-token"])]
                                           (if (not token)
                                             (make-response {:ret :failed, :msg "Token missing in request"})
                                             (product-groups env token))))}}]

Javascript example:

router.get('/product-groups', verifyToken, async (req, res, next) => {
  try {
    const productGroups = await getProductGroups();
    const ret = { ret: 'ok', product_groups: productGroups };
    res.status(200).json(ret);
  } catch (err) {
    next(err);
  }
});

Backend - Testing

Both ecosystems provide excellent tools for both unit and integration testing. See:

Javascript example:

it('Call /product-groups', async () => {
  const res = await axios.post(`${baseUrl}/login`, { username: 'jartsa', password: 'joo' });
  const { token } = res.data;
  await spec()
    .get(`${baseUrl}/product-groups`)
    .withHeaders({ 'x-token': token })
    .expectStatus(200)
    .expectJsonMatch({ ret: 'ok', product_groups: [{ pgId: 1, name: 'Books' }, { pgId: 2, name: 'Movies' }] });
});

Clojure example:

(deftest product-groups-test
  (log/debug "ENTER product-groups-test")
  (testing "GET: /api/product-groups"
    (let [creds {:username "jartsa" :password "joo"}
          login-ret (test-config/call-api :post "login" nil creds)
          _ (log/debug (str "Got login-ret: " login-ret))
          token (get-in login-ret [:body :token])
          params {:x-token token}
          get-ret (test-config/call-api :get "/product-groups" params nil)
          status (:status get-ret)
          body (:body get-ret)
          right-body {:ret "ok", :product-groups [{:pgId 1, :name "Books"} {:pgId 2, :name "Movies"}]}]
      (is (= true (not (nil? token))))
      (is (= 200 status))
      (is (= right-body body)))))

Backend - Programming Experience

You just can’t beat Clojure’s programming at the REPL. REPL driven development is almost impossible to explain to someone who haven’t used a Lisp with an editor with a good REPL integration. The real interaction with the running program is something that you just can’t experience with other languages.

But I must admit that Javascript / Node is not bad. The development cycle is really fast since Node starts with code chantges blazingly fast. But still, there is no REPL.

Frontend - Language

In this example I used Typescript in the frontend side so let’s use it in this comparison.

Typescript is not bad at all. There are a lot of resources on how to learn to program using Typescript and e.g., React. Using those resources you can start being productive quite soon. E.g. I started learning Typescript / React on December and I was writing production software on January. Of course, I’m not as fast as those guys who have been programming Typescript / React for years, but I’m getting there day by day.

Using Clojurescript in the frontend side is really enjoyable. I had an overall feeling that there is more boilerplate in the Typescript side.

Clojurescript example:

...
(defn login []
  (let [login-data (r/atom (empty-creds))]
    (fn []
      (let [_ (f-util/clog "ENTER login")
            title "You need to login to use the web store"
            {:keys [ret _msg] :as _r-body} @(re-frame/subscribe [::login-response])
            _ (when (= ret :ok) (re-frame/dispatch [::f-state/navigate ::f-state/product-groups]))]
        [:div.app
         [:div.p-4
          [:p.text-left.text-lg.font-bold.p-4 title]
          (when (= ret :failed)
            [:div {:className "flex grow w-3/4 p-4"}
             (re-frame/dispatch [::save-username nil])
             [f-util/error-message "Login failed!" "Username or password is wrong."]])]
         [:div.flex.grow.justify-center.items-center
          [:div {:className "flex grow w-1/2 p-4"}
           [:form
            [:div.mt-3
             [f-util/input "Username" :username "text" login-data]
             [f-util/input "Password" :password "password" login-data]
             [:div.flex.flex-col.justify-center.items-center.mt-5
              [:button {:className "login-button"
                        :on-click (fn [e]
                                    (.preventDefault e)
                                    (re-frame/dispatch [::login-user @login-data])
                                    (re-frame/dispatch [::save-username (:username @login-data)])
                                    )}
               "Login"]]]]]]]))))

Typescript example:

...
  return (
    <>
      <Header />
      <div className="p-4">
        <p className="text-left text-lg font-bold p-4">{title}</p>
      </div>
      {error && (
        <div className="flex grow w-3/4 p-4">
          <ErrorMessage title={error.title} msg={error.msg} />
        </div>
      )}
      <div className="flex grow justify-center items-center">
        <div className="flex grow w-1/2 p-4">
          <form onSubmit={handleSubmit}>
            <div className="mt-3">
              <div className="flex flex-wrap gap-2 items-center mt-1">
                <label htmlFor="username" className="login-label">
                  Username
                </label>
                <div className="">
                  <input
                    type="text"
                    id="username"
                    className="login-input"
                    placeholder="username"
                    required
                  />
                </div>
              </div>
              <div className="flex flex-wrap gap-2 items-center mt-1">
                <label htmlFor="password" className="login-label">
                  Password
                </label>
                <div className="">
                  <input
                    type="text"
                    id="password"
                    className="login-input"
                    placeholder="password"
                    required
                  />
                </div>
              </div>
            </div>
            <div className="flex flex-col justify-center items-center mt-5">
              <button className="w-32">Login</button>
            </div>
          </form>
        </div>
      </div>
    </>
  );
}

Frontend - Tooling

Both Typescript and Clojurescript transpile to Javascript, so they both use npm as the package manager.

Clojurescript has several build tools, I used shadow-cljs in this exercise.

Javascript/Typescript also has several build tools, I used Vite in this exercise.

Both application can use browser React developer tools, of course.

Frontend - Libraries

Both Javascript/Typescript and Clojurescript have excellent frontend libraries. You can use React in both sides, with Typescript as is, and with Clojurescript the Reagent minimal React wrapper.

For state management I used Redux Toolkit in the Typescript side, and re-frame in the Clojurescript side. I had a feeling that re-frame was simpler to use and required less boiler-plate and cognitive burden.

Frontend - Routing and Connecting to Backend

I used metosin/reitit in the Clojurescript side as a routing library, and React Router in the Typescript side. Both were quite straightforward to use.

Clojurescript example:

(def routes-dev
  ["/"
   [""
    {:name ::f-state/home
     :view home-page
     :link-text "Home"
     :controllers
     [{:start (fn [& params] (js/console.log (str "Entering home page, params: " params)))
       :stop (fn [& params] (js/console.log (str "Leaving home page, params: " params)))}]}]
   ["login"
      {:name ::f-state/login
       :view f-login/login
       :link-text "Login"
       :controllers [{:start (fn [& params] (js/console.log (str "Entering login, params: " params)))
                      :stop (fn [& params] (js/console.log (str "Leaving login, params: " params)))}]}]
   ["product-group"
      {:name ::f-state/product-groups
       :view f-product-group/product-groups
       :link-text "Product group"
       :controllers [{:start (fn [& params] (js/console.log (str "Entering product-group, params: " params)))
                      :stop (fn [& params] (js/console.log (str "Leaving product-group, params: " params)))}]}]
   ["products/:pgid"
      {:name ::f-state/products
       :parameters {:path {:pgid int?}}
       :view f-products/products
       :link-text "Products"
       :controllers [{:start (fn [& params] (js/console.log (str "Entering products, params: " params)))
                      :stop (fn [& params] (js/console.log (str "Leaving products, params: " params)))}]}]
   ["product/:pgid/:pid"
      {:name ::f-state/product
       :parameters {:path {:pgid int?
                           :pid int?}}
       :view f-product/product
       :link-text "Product"
       :controllers [{:start (fn [& params] (js/console.log (str "Entering product, params: " params)))
                      :stop (fn [& params] (js/console.log (str "Leaving product, params: " params)))}]}]])

Typescript example:

const router = createBrowserRouter([
  {
    path: "/",
    element: <Index />,
  },
  {
    path: "login",
    element: <Login />,
  },
  {
    path: "product-groups",
    element: <ProductGroups />,
  },
  {
    path: "products/:pgId",
    element: <Products />,
  },
  {
    path: "product/:pgId/:pId",
    element: <Product />,
  },
]);

Frontend - Using React

I wrote a dedicated blog post which illustrates in more detail how one can use a third-party React library from Typescript vs Clojurescript. You might want to read it, too: Clojurescript / Javascript Interop with React Components.

NOTE: As of writing this Reagent does not support the new React 18 rendering API yet, so I used React 17 with the Clojurescript implementation.

Frontend - HTML and CSS

I used Tailwind in both applications. Using Clojurescript you write ordinary Clojure code (using weavejester/hiccup), ie., just ordinary Clojure vectors and maps:

(defn header []
  (fn []
    (let [login-status @(re-frame/subscribe [::f-state/login-status])
          username @(re-frame/subscribe [::f-state/username])]
      [:div.flex.grow.bg-gray-200.p-4
       [:div.flex.flex-col.grow
        [:div.flex.justify-end
         (when (= login-status :logged-in)
           [:div.flex.justify-right.gap-2
            [:p username]
            ;; NOTE: CSS for a is defined in app.css
            [:a {:on-click #(re-frame/dispatch [::f-state/logout]) } "Logout"]])]
        [:div.flex.justify-center
         [:h1.text-3xl.text-center.font-bold "Demo Webtore"]]]
       ])))

So, e.g. a div with “flex grow bg-gray-200 p-4” can be written as a vector with the keyword :div.flex.grow.bg-gray-200.p-4.

In the Typescript implementation the same header implementation using JSX:

  return (
    <div className="flex grow bg-gray-200 p-4">
      <div className="flex flex-col grow">
        <div className="flex justify-end">
          {loginState === "loggedIn" && user && (
            <div className="flex justify-right gap-2">
              <p className="">{user}</p>
              <a href="#" onClick={handleLogout} className="font-medium text-blue-600 dark:text-blue-500 hover:underline">Logout</a>
            </div>
          )}
        </div>
        <div className="flex justify-center">
          <h1 className="text-3xl text-center font-bold">Demo Webstore!</h1>
        </div>
      </div>
    </div>
  );

The Clojurescript implementation is more concise, and since the Clojurescript frontend uses just Clojure datastructures, it is easier to manipulate the frontend code using Clojure libraries.

Linting

I used eslint in the Typescript side, and clj-kondo in the Clojurescript side.

Frontend - Programming Experience

The programming experience in both applications was pretty good. Maybe the boiler-plate with Typescript/React made the cognitive burden a bit higher in that side. But I might be a bit biased here since I don’t have that much Typescript/React experience.

Similarities

Both ecosystems get the job done. You can be very productive in both ecosystems when you have enough experience.

Differences

Clojure being a Lisp provides excellent REPL experience. But Javascript/Typescript is not a bad language at all.

The main difference is in the REPL driven development experience. In the Clojure side clojurians tend to interact with the living system using the REPL. In the Javascript side you just let nodemon to watch the changes and start the server from scratch and see if it works this time.

I left some REPL experimentations as rich comments at the end of the Clojure files, example domain.clj:

(comment
...
  (let [products (:products (get-domain-data "resources/data"))]
    (first (filter (fn [item] (and (= (:pgId item) 2) (= (:pId item) 49))) products)))
...
)

Final Conclusions

Both Javascript and Clojure are good ecosystems and languages in the environment where they do their work best. So, I just provide some final recommendations in which environments to use a particular language.

  • If you want to stay on the safe side: choose Javascript ecosystem. Most software shops use mainstream languages, and Javascript ecosystem definitely is mainstream. It is easier to find Javascript jobs than Clojure jobs.
  • If you need to do concurrent programming: Clojure. Read the more detailed explanation in Concurrent Programming.
  • If you are looking for a new interesting programming language which teaches you functional programming: Clojure.
  • If you do typical enterprise software / full-stack applications: pick either Javascript ecosystem or Clojure / Clojurescript. Both will get the job done. But with Clojure you might have more challenges to find programmers when scaling the team.

Disclaimer

Because I have noticed that there are many developers who think of their favorite language with great religious-like affection I must add this disclaimer: The opinions in this blog article are very personal. Do not get offended.

The writer is working at a major international IT corporation building cloud infrastructures and implementing applications on top of those infrastructures.

Kari Marttila

Kari Marttila’s Home Page in LinkedIn: https://www.linkedin.com/in/karimarttila/