Front-End Integration with ClojureScript

Learn how ClojureScript fits into a modern Clojure web stack, where shared code really helps, and how to structure browser-server boundaries without turning the app into one blurred codebase.

ClojureScript: The Clojure dialect that compiles to JavaScript so you can write browser code with the same core language ideas used on the server.

ClojureScript is most powerful when you use it to align data modeling and application structure across client and server without pretending the browser and the backend are the same runtime. Good full-stack Clojure design shares what truly belongs together, keeps the HTTP boundary explicit, and avoids leaking browser concerns into the service layer or service concerns into the UI.

What Full-Stack Clojure Really Means

Using Clojure on the server and ClojureScript in the browser does not automatically mean you should share everything. It means you have the option to share:

  • validation rules
  • shape transformations
  • domain constants
  • small pure helper logic

The visual below shows the healthy boundary: browser UI on one side, server-side business and persistence on the other, and a narrow band of truly shared pure code between them.

Full-stack Clojure and ClojureScript boundaries with shared .cljc code and explicit browser-server contracts

Reagent, Re-frame, and the UI Layer

Reagent is the common lightweight way to build React-backed UIs in ClojureScript. It is a good fit when you want a small functional view layer and explicit control over composition.

Re-frame adds a more structured event-and-subscription model. That is useful when the browser application becomes large enough that event flow, effect handling, and state derivation need stronger discipline.

The practical distinction is:

  • choose Reagent when a small or medium UI mostly needs composable components
  • add Re-frame when the client-side state flow itself becomes an architectural problem

Another useful question is whether the browser is primarily:

  • enhancing a server-rendered experience
  • hosting a medium-sized interactive client
  • acting as a large application shell with its own data flow, routing, and effect model

The larger the client becomes, the more explicit its event and state architecture needs to be.

Shared Code Works Best When It Is Pure

The safest code to share across Clojure and ClojureScript lives in .cljc files and stays free of platform-specific APIs.

1(ns my-app.shared.validation)
2
3(defn valid-email? [email]
4  (boolean
5   (re-matches #".+@.+\\..+" email)))

That kind of function is an ideal shared candidate because it does not care whether it runs in the browser or on the server.

Bad shared-code candidates usually include:

  • DOM manipulation
  • database access
  • HTTP client calls
  • file system access
  • server-only auth logic

Good teams also keep shared code small on purpose. A little shared logic can remove real duplication. Too much shared code can blur responsibility until browser, server, and domain concerns are all mixed together.

Build Tooling Should Stay Boring

For modern ClojureScript applications, shadow-cljs is usually the practical build default because it handles browser targets, npm interop, hot reload workflows, and module output well.

Small demos can still use simpler flows, but once the app has real frontend complexity, stable build tooling matters more than cleverness.

A minimal shadow-cljs.edn browser target looks like this:

1{:source-paths ["src/main" "src/shared"]
2 :dependencies [[reagent "1.2.0"]]
3 :builds
4 {:app
5  {:target :browser
6   :output-dir "resources/public/js"
7   :asset-path "/js"
8   :modules {:main {:init-fn my-app.frontend/init}}}}}

The API Boundary Still Matters

Even when both sides use Clojure-family languages, the browser-server contract should remain explicit. The server should not simply expose internal data structures because “the client can read EDN anyway.”

Design the boundary deliberately:

  • use stable response shapes
  • version API behavior when needed
  • keep server authority over security-sensitive decisions
  • share validation and transformation logic only where it actually reduces duplication

JSON is still the most common wire format for mixed ecosystems. EDN can be excellent for internal tools or Clojure-heavy systems, but its main value comes from control over both sides of the boundary.

It also helps to decide early whether the browser is consuming:

  • a classic JSON API
  • an event or websocket feed
  • server-rendered HTML plus progressive enhancement

Different choices imply different caching, routing, auth, and deployment trade-offs. Shared language does not erase those design questions.

Decide What the Browser Should Own

The client should usually own:

  • view state
  • local interaction flow
  • optimistic or pending UI details
  • presentation-specific derivations

The server should usually own:

  • authorization
  • authoritative persistence
  • audit-sensitive decisions
  • integration with protected infrastructure

The more clearly those responsibilities are divided, the less likely the full-stack application is to become one blurred codebase with no clean boundary.

When ClojureScript Is Worth the Cost

ClojureScript is not required for every Clojure web system. It tends to pay off most when:

  • the browser experience is genuinely interactive
  • the team values REPL-driven feedback in the client too
  • shared data shapes or validation rules remove real duplication
  • the team wants one language family across the stack and can sustain the tooling

It pays off less when the application is mostly server-rendered pages with only light interactivity. In those cases, the simpler stack often wins.

Common Mistakes

Sharing Too Much

The fact that .cljc exists does not mean UI logic and backend policy logic belong in the same namespace. Share only what is genuinely cross-runtime and pure.

Treating the Client as Trusted

Client-side validation improves UX, but server-side validation and authorization still own correctness and security.

Building a Large Browser App Without a State Model

A small Reagent app can stay simple. A growing application usually needs clearer event, subscription, and effect boundaries before it becomes hard to reason about.

Assuming Shared Language Means Shared Authority

Using Clojure on both sides improves ergonomics, but it does not make the browser trusted or the server optional. Shared code should support the contract, not dissolve it.

Practical Heuristics

Use ClojureScript when you want a richer client and value the same language family across the stack. Share pure logic, not infrastructure concerns. Keep browser state, server authority, and persistence responsibilities clearly separated. When in doubt, design the API contract first and let shared code support it rather than define it.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026