Skip to main content
Test Double company logo
Services
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say Hello
Test Double logo
Menu
Services
BackGrid of dots icon
Services Overview
Holistic software investment consulting
Software Delivery
Accelerate quality software development
Product Management
Launch modern product orgs
Legacy Modernization
Renovate legacy software systems
Cycle icon
DevOps
Scale infrastructure smoothly
Upgrade Rails
Update Rails versions seamlessly
Technical Recruitment
Build tech & product teams
Technical Assessments
Uncover root causes & improvements
Case Studies
Solutions
Solutions
Accelerate Quality Software
Software Delivery, DevOps, & Product Delivery
Maximize Software Investments
Product Performance, Product Scaling, & Technical Assessments
Future-Proof Innovative Software
Legacy Modernization, Product Transformation, Upgrade Rails, Technical Recruitment
About
About
About
What's a test double?
Approach
Meeting you where you are
Founder's Story
The origin of our mission
Culture
Culture
Culture & Careers
Double Agents decoded
Great Causes
Great code for great causes
EDI
Equity, diversity & inclusion
Insights
Insights
All Insights
Hot takes and tips for all things software
Leadership
Bold opinions and insights for tech leaders
Developer
Essential coding tutorials and tools
Product Manager
Practical advice for real-world challenges
Say hello
Developers
Developers
Developers
Software tooling & tips

How Clojure enhances Node.js with isomorphic scripting

Join us as we delve into the world of isomorphic ClojureScript, showing how Clojure and Node.js can create powerful, efficient applications.
Andy Vida
|
January 20, 2016
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

I've recently returned from CodeMash 2.0.1.6 where I gave a talk entitled "Bringing the Power of Clojure to Node.js."  While preparing for it, I knew I wanted to show more real-world scenarios vs. basic "Hello World" ones you can find many other places.

I prepared a demo of an isomorphic ClojureScript application. Blow I'll share it in more detail than I could during the talk.

What is isomorphic ClojureScript?

Isomorphic ClojureScript is compiled Clojure targeting JavaScript where the same code runs on both the client and server.

Benefits of an isomorphic approach

This approach has many benefits.  One huge one is what we already mentioned - code sharing.

With many Single Page Application-type architectures, when a user first hits the page, all of the assets have to download, usually taking several seconds.  While content is loading, the page is unrendered. A benefit of an isomorphic approach is that you get the performance of rendering on the server and you can render components after the page loads on the client.

Creating an isomorphic ClojureScript app

For this application, I'm going to assume that you already have Java (> 1.7.0), Leiningen (2.5.3) and Node.js (v5.4.1) already installed.

Create new project

Lets create a new project using Leiningen and the application template with lein new app demo which will set up the project structure below.

.
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
│   └── intro.md
├── project.clj
├── resources
├── src
│   └── demo
│       └── core.clj
└── test
    └── demo
        └── core_test.clj

Since we'll be targeting Node.js, we need to add a package.json file at the project root.  Copy the code below and place it in the newly created file.  Take note of the Node.js version and the devDependencies. Only the current stable versions of Node.js are supported.  Node also has great source map support, and is enabled by adding a dev dependency.

{
  "name": "demo",
  "version": "0.1.0",
  "private": "true",
  "engines": {
    "node": "0.12.x"
  },
  "dependencies": {},
  "devDependencies": {
    "source-map-support": "0.4.0",
    "ws": "0.8.1"
  }
}

We can also delete the template Clojure code, as we'll be replacing it with ClojureScript.

rm -rf src/ test/

Create an express app

We'll be using Express as our Node.js web application framework.  Run the following command to install it.

npm install express --save

Now that it's installed, let's create a new server and a single route.  Create a new file src-server/demo/server.cljs and add the following code:

; src-server/demo/server.cljs

(ns demo.server
  (:require [cljs.nodejs :as nodejs]))

(nodejs/enable-util-print!)

(def express (nodejs/require "express"))

(defn say-hello! [req res]
  (.send res "Hello world!"))

(defn -main []
  (let [app (express)]
    (.get app "/" say-hello!)
    (.listen app 3000 (fn []
                        (println "Server started on port 3000")))))

(set! *main-cli-fn* -main)

Let's take a look at this file.  We declare a new namespace and require Node. We also include Express so that we can set up our server.  In the -main function, we set up a root route and when it's requested by the browser, call a function named say-hello! which returns the text "Hello World"

We have the code to start the server, but can't actually fire it up yet.  Let's do that next by configuring cljs-build. Open project.clj and add the following:

; project.clj

(defproject demo "0.1.0-SNAPSHOT"
  :description "FIXME: write this!"
  :url "http://example.com/FIXME"

  :min-lein-version "2.5.3"

  :dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/clojurescript "1.7.170"]]

  :plugins [[lein-cljsbuild "1.1.1"]]


  :clean-targets ^{:protect false} ["resources"]

  :cljsbuild {
    :builds [{:id "server"
              :source-paths ["src-server"]
              :compiler {
                :main demo.server
                :output-to "resources/public/js/server-side/server.js"
                :output-dir "resources/public/js/server-side"
                :target :nodejs
                :optimizations :none
                :source-map true}}]})

We add a new build named server and tell the compiler to output our compiled file to resources/public/js/server-side/server.js. Here we also specify the target of this build to Node by setting :target :nodejs.  The :main demo.server-side tells the compiler where to find the main function to call when starting the Node application.

Now that everything is wired up, let's test out this simple server and route. First, build the app by lein cljsbuild once server.  You should see that the output is placed where we set it in project.clj. Now, start up the Node application with node resources/public/js/server-side/server.js. The server should have started on port 3000.  Open up your browser to http://localhost:3000 and you should see "Hello World!" Congrats!  You have a working ClojureScript application running on Node.js!

Create a server rendered page

We have our server up and running, but let's go even further by creating a server rendered page.  For this example, we'll be using Reagent, a minimalistic interface between ClojureScript and React.js.

In order to use Reagent, it's necessary to install React.js on the server side as well.  Install it by executing:

npm install react react-dom --save

and add Reagent to project.clj

:dependencies [[org.clojure/clojure "1.7.0"]
               [org.clojure/clojurescript "1.7.170"]
               [reagent "0.6.0-alpha"]]

Change src-server/demo/server.cljs to require Reagent and call a function handle-request that will render a static page from the server side:

; src-server/demo/server.cljs

(ns demo.server
  (:require [cljs.nodejs :as nodejs]
            [reagent.core :as reagent]))

(nodejs/enable-util-print!)

(def express (nodejs/require "express"))

(defn template []
  [:html
   [:head
    [:meta {:charset "utf-8"}]
    [:meta {:name    "viewport"
            :content "width=device-width, initial-scale=1.0"}]]
   [:body
    [:div#app
      [:h1 "Server Rendering!!!"]]]])

(defn ^:export render-page [path]
  (reagent/render-to-static-markup [template]))

 (defn handle-request [req res]
   (.send res (render-page (.-path req))))

(defn -main []
  (let [app (express)]
    (.get app "/" handle-request)
    (.listen app 3000 (fn []
                        (println "Server started on port 3000")))))

(set! *main-cli-fn* -main)

You'll see that Reagent has a render-to-static-markup function that mirrors the React.js ReactDOMServer.renderToStaticMarkup function to render a static page. Also of note, Reagent uses a Hiccup-like syntax to describe the UI.

If you build (lein cljsbuild once server) and run the application (node resources/public/js/server-side/server.js) as before, you should see output similar to the image below.  Check out the network tab - there isn't any JavaScript sent to the client at this time.

Server rendered code

Create client side application

We've increased performance by rendering the initial page on the server, now we'd like to render other components on the client side.  Let's create our client side application.

First, let's do some refactoring. Create a new namespace (ns site.tools) and move some functions from (ns demo.server) there.

; src/site/tools.cljs

(ns site.tools
  (:require [reagent.core :as reagent]))

(enable-console-print!)

(defn template []
  [:html
   [:head
    [:meta {:charset "utf-8"}]
    [:meta {:name    "viewport"
            :content "width=device-width, initial-scale=1.0"}]]
   [:body
    [:div#app
      [:h1 "Server Rendering!!!"]]]])

(defn ^:export render-page [path]
  (reagent/render-to-static-markup (template)))
; src-server/demo/server.cljs

(ns demo.server
  (:require [cljs.nodejs :as nodejs]
            [site.tools :as tools]))

(nodejs/enable-util-print!)

(def express (nodejs/require "express"))

(defn handle-request [req res]
 (.send res (tools/render-page (.-path req))))

(defn -main []
  (let [app (express)]
    (.get app "/" handle-request)
    (.listen app 3000 (fn []
                        (println "Server started on port 3000")))))

(set! *main-cli-fn* -main)

Next, we'll need to add Secretary for basic client side routing and dispatch as well as Pushy for HTML5 push state.

; project.clj

:dependencies [[org.clojure/clojure "1.7.0"]
                 [org.clojure/clojurescript "1.7.170"]
                 [reagent "0.6.0-alpha"]
                 [secretary "1.2.3"]
                 [kibu/pushy "0.3.1"]]

Now, let's create some client side markup.  We'll define a default route, a Reagent atom to keep state, a function app-view that we'll call to determine the page requested, and some basic markup to display for that page.

; src/demo/core.cljs

(ns demo.core
  (:require [reagent.core :refer [atom]]
            [secretary.core :as secretary :refer-macros [defroute]]))

(def current-page (atom nil))

(defn home-page []
  [:div [:h1 "Home Page"]])

(defn app-view []
  [:div [@current-page]])

(secretary/set-config! :prefix "/")

(defroute "/" []
  (.log js/console "home page")
  (reset! current-page home-page))

; the server side doesn't have history, so we want to make sure current-page is populated
(reset! current-page home-page)

‍
In our server side template, we've previously defined a div with the id app to use for our client side application render to.  We have just created some basic markup for it, but now we need to actually render the markup.  Create a new namespace where we'll use Reagent to render a component to the div.  You'll see we call the app-view function from here to render the markup for the page requested.

; src-client/demo/client.cljs

(ns demo.client
  (:require [reagent.core :as reagent]
            [secretary.core :as secretary]
            [pushy.core :as pushy]
            [demo.core :as core])
  (:import goog.History))

(enable-console-print!)

(reagent/render-component [core/app-view] (.getElementById js/document "app"))

(pushy/push-state! secretary/dispatch!
  (fn [x] (when (secretary/locate-route x) x)))

Next, we'll change our render-page function to dispatch the default client route and render the home page markup on initial page load.

; src/site/tools.cljs

(ns site.tools
  (:require [reagent.core :as reagent]
            [secretary.core :as secretary]
            [demo.core :as core]))

(enable-console-print!)

(defn template [{:keys [body]}]
  [:html
   [:head
    [:meta {:charset "utf-8"}]
    [:meta {:name    "viewport"
            :content "width=device-width, initial-scale=1.0"}]]
   [:body
    [:div#app [body]]
    [:script {:type "text/javascript"
              :dangerouslySetInnerHTML {:__html "goog.require('demo.client');"}}]]])

(defn ^:export render-page [path]
  (reagent/render-to-static-markup (do
                                     (secretary/dispatch! path)
                                     [template {:body core/app-view}])))

We've created our client side application, and now we need to compile it and output it to a location.  We'll add a new build configuration named app for this.

:cljsbuild {
    :builds [{:id "server"
              :source-paths ["src" "src-server"]
              :compiler {
                :main demo.server
                :output-to "resources/public/js/server-side/server.js"
                :output-dir "resources/public/js/server-side"
                :target :nodejs
                :optimizations :none
                :source-map true}}
             {:id "app"
              :source-paths ["src" "src-client"]
              :compiler {
                :output-to "resources/public/js/app.js"
                :output-dir "resources/public/js"
                :optimizations :none
                :source-map true}}]}

Check out the :source-paths section of each build.  You'll see that we've separated the server and client code as well as included shared code in each build. This is typical of an isomorphic app where code runs on both the client and server.

Because we've built these separately, as it stands now, the server has no idea that there's client side code.  We'll need to tell Express to serve up the location of where to find the client side code.  We'll use an NPM module called serve-static to do this. Require the module in demo.server and tell Express to create a route that matches the output directory specified in the build configuration.

npm install serve-static --save

; src-server/demo/server.cljs

(ns demo.server
  (:require [cljs.nodejs :as nodejs]
            [site.tools :as tools]))

(nodejs/enable-util-print!)

(def express (nodejs/require "express"))
(def serve-static (nodejs/require "serve-static"))

(defn handle-request [req res]
 (.send res (tools/render-page (.-path req))))

(defn -main []
  (let [app (express)]
    (.get app "/" handle-request)
    (.use app (serve-static "resources/public/js"))
    (.listen app 3000 (fn []
                        (println "Server started on port 3000")))))

(set! *main-cli-fn* -main)

Next, add a script tag to include the compiled client side code.

; src/site/tools.cljs

(ns site.tools
  (:require [reagent.core :as reagent]
            [secretary.core :as secretary]
            [demo.core :as core]))

(enable-console-print!)

(defn template [{:keys [body]}]
  [:html
   [:head
    [:meta {:charset "utf-8"}]
    [:meta {:name    "viewport"
            :content "width=device-width, initial-scale=1.0"}]]
   [:body
    [:div#app [body]]
    [:script {:type "text/javascript" :src "goog/base.js"}]
    [:script {:type "text/javascript" :src "app.js"}]
    [:script {:type                    "text/javascript"
              :dangerouslySetInnerHTML {:__html "goog.require('demo.client');"}}]]])

(defn ^:export render-page [path]
  (reagent/render-to-static-markup (do
                                     (secretary/dispatch! path)
                                     [template {:body core/app-view}])))

We've finally got everything wired up. Our build command has changed though. Now we build both the server and client sides with:

lein cljsbuild once app server

...and we can run the server code in the same old way:

node resources/public/js/server-side/server.js.

You should see "home page" output to both the browser and server consoles.

client rendered code 1

Now we could totally stop here because we have a functioning isomorphic application, but lets add some other goodies.

Add a new client side route and some navigation to (ns demo.core).

; src/demo/core.cljs

(ns demo.core
  (:require [reagent.core :refer [atom]]
            [secretary.core :as secretary :refer-macros [defroute]]))

(def current-page (atom nil))

(defn navigation []
  [:div [:a {:href "/"} "Home Page"]
   [:span {:style {:padding "5px"}}]
   [:a {:href "/page-one"} "Page One"]
   [:span {:style {:padding "5px"}}]])

(defn home-page []
  [:div [navigation] [:h1 "Home Page"]])

(defn page-one []
  [:div [navigation] [:h1 "Page One"]])

(defn app-view []
  [:div [@current-page]])

(secretary/set-config! :prefix "/")

(defroute "/" []
  (.log js/console "home page")
  (reset! current-page home-page))

  (defroute "/page-one" []
    (.log js/console "page-one")
    (reset! current-page page-one))

; the server side doesn't have history, so we want to make sure current-page is populated
(reset! current-page home-page)

Build both the app and server then fire up the Node application once again. You'll see the home page along with the new navigation. Click "Page One".  You'll see "Page One" in browser console, but not in server console. All is now rendering client side.

Client rendered code 2

Now for a little fun and to show off some basic JavaScript interop, let's add an alert.

; src/demo/core.cljs

...

(defn navigation []
  [:div [:a {:href "/"} "Home Page"]
   [:span {:style {:padding "5px"}}]
   [:a {:href "/page-one"} "Page One"]
   [:span {:style {:padding "5px"}}]
   [:a {:href "#" :on-click #(js/alert "HEY, I WORK!!")} "Say Hello"]])

...

Rebuild the app and server and start the app once more.

client rendered code 3

Next steps

I feel that the future is bright for ClojureScript. Its story fits really well with the problems most developers are facing today - especially in the dawn of the React era. I hope that this demo has shown you ClojureScript's simplicity along with the power it can provide.

The complete application can be found on GitHub.

Related Insights

🔗
Build and deploy Node.js projects on Raspberry Pi
🔗
How to test logging in Node.js: practical TDD tips and tricks

Explore our insights

See all insights
Leadership
Leadership
Leadership
The business of AI: Solve real problems for real people

After participating in the Perplexity AI Business Fellowship, one thing became clear: the AI hype cycle is missing the business fundamentals. Here are 3 evidence-based insights from practitioners actually building or investing in AI solutions that solve real problems.

by
Cathy Colliver
Leadership
Leadership
Leadership
Pragmatic approaches to agentic coding for engineering leaders

Discover essential practices for AI agentic coding to enhance your team’s AI development learning and adoption, while avoiding common pitfalls of vibe coding.

by
A.J. Hekman
by
Aaron Gough
by
Alex Martin
by
Dave Mosher
by
David Lewis
Developers
Developers
Developers
16 things software developers believe, per a Justin Searls survey

Ruby on Rails developer Justin Searls made a personality quiz, and more than 7,000 software developers filled it out. Here's what it revealed.

by
Justin Searls
Letter art spelling out NEAT

Join the conversation

Technology is a means to an end: answers to very human questions. That’s why we created a community for developers and product managers.

Explore the community
Test Double Executive Leadership Team

Learn about our team

Like what we have to say about building great software and great teams?

Get to know us
Test Double company logo
Improving the way the world builds software.
What we do
Services OverviewSoftware DeliveryProduct ManagementLegacy ModernizationDevOpsUpgrade RailsTechnical RecruitmentTechnical Assessments
Who WE ARE
About UsCulture & CareersGreat CausesEDIOur TeamContact UsNews & AwardsN.E.A.T.
Resources
Case StudiesAll InsightsLeadership InsightsDeveloper InsightsProduct InsightsPairing & Office Hours
NEWSLETTER
Sign up hear about our latest innovations.
Your email has been added!
Oops! Something went wrong while submitting the form.
Standard Ruby badge
614.349.4279hello@testdouble.com
Privacy Policy
© 2020 Test Double. All Rights Reserved.