Heya, sorry this is quite long. I am a hobby programmer. I watched CljureScript in the Age of TypeScript – David Nolen (https://www.youtube.com/watch?v=3HxVMGaiZbc). I really liked it and wanting to give it a go with a web application. So I have build a web application that has a Component Library with Typescript, a frontend application with Clojurescript, and a backend with Clojure. Component library I build the React component library using: • TypeScript + Vite + Storybook • TailwindCSS + shadcn/ui • Zod for validation • react-hook-form for form handling • pragmatic-drag-and-drop for DnD interactions Rather than exporting individual components, the library exposes full screen components (obvioulsy composed of smaller parts (buttons, inputs, etc.)). These screens are controlled entirely via props — often a large number — for content and event handlers. There is definetly some prop drilling. Everything stays mostly stateless and declarative. But in practice, it starts to feel stateful in some places: Forms involve a back-and-forth between zod, react-hook-form, and the consuming clojurescript app. Drag-and-drop also requires coordination across both the component library and the ClojureScript app. The library is built with Vite, and I use yalc to link it into a ClojureScript (re-frame) frontend. ClojureScript Frontend The frontend uses shadow-cljs, uix2, re-frame, and reitit-frontend. I access the component library via yalc, and copy the TailwindCSS output from node_modules/component-ui/dist/style.css to public/main.css. Then I run shadow-cljs -A:dev watch learningnow to start the app. The project is structured with a state/ folder for all re-frame logic and a views/ folder where each file represents a screen. Each view pulls in a full-screen component from the UI library and passes in the required props. Some things get tricky—like managing prop names, which come from TypeScript in camelCase and must be mapped correctly in CLJS. Forms can also be awkward, especially when dealing with file uploads using (js/FormData.), or integrating validation logic from tools like Zod and react-hook-form. These kinds of stateful interactions blur the line between the “stateless” component library and the stateful ClojureScript app, and can feel clunky to manage. Here is an example of a screen view in the clojurescript app
(ns learningnow.views.more-view
(:require
["brainbreaker-ui" :refer [More]]
[learningnow.auth0 :as auth0]
[learningnow.hooks :as hooks]
[learningnow.state.account :as account]
[learningnow.state.more :as more]
[re-frame.core :as rf]
[uix.core :as uix :refer [$ defui]]
[uix.dom]))
(defui more []
(let [user-data (hooks/use-subscribe [::account/user-data])
status (hooks/use-subscribe [::account/subscription-type])
loggedIn (some? user-data)
formOpened (hooks/use-subscribe [::more/toggle-form-status])]
($ More (clj->js {:userId (:sub user-data)
:loggedIn loggedIn
:status status
:userEmail (:email user-data)
:changeEmail (fn [new-email] (rf/dispatch [::auth0/change-email new-email]))
:toggleForm (fn [] (rf/dispatch [::more/toggle-form]))
:formOpened formOpened
:changePassword #(rf/dispatch [::auth0/request-password-reset])
:signOut #(rf/dispatch [::auth0/logout "You have been logged out successfully."])
:manageSubscription #(rf/dispatch [::more/manage-subscription])
:privacy #(rf/dispatch [:routes/navigate :routes/#privacy])
:terms #(rf/dispatch [:routes/navigate :routes/#terms])
:deleteAccount #(rf/dispatch [::more/delete-account])}))))
Other things that have made this project a bit confusing are setting up Auth0 and Stripe, both of which I integrated using their JavaScript SDKs via interop (thanks to people publishing examples online). For Auth0, I used the @auth0/auth0-spa-js package and managed authentication state using JavaScript interop in ClojureScript. Once the user is logged in, I extract the token and use it in my re-frame HTTP calls by adding an Authorization header like {"Authorization" (str "Bearer " token)}. It works, but managing this across the JS and CLJS boundary—especially keeping the token state in sync—adds complexity that’s easy to get wrong. It can also feel like making a change means I need to make changes in multiples places (again a result of doing this solo).
Clojure backend
This project is based on Jacek Schae’s Datomic course repo (https://github.com/jacekschae/learn-datomic-course-files; I used datomc pro though) and is essentially an API deployed using Ansible. Getting Stripe (used com.stripe/stripe-java) and Auth0 working was also a challenge, especially integrating them cleanly with the backend. In hindsight, Datomic might not have been the best choice—it requires two JVM processes to run, which feels a bit heavy for this kind of project.
Question
I’d really like to hear people’s thoughts or feedback on this setup.
As a solo dev, I sometimes wonder if I’m just making things unnecessarily hard for myself. I’ve ended up with three separate projects:
A TypeScript component library (UI-only)
A ClojureScript frontend (re-frame)
A Clojure backend API (Datomic, Stripe, Auth0)
There’s a strong appeal in the separation of concerns — having a clean, stateless UI library, a structured frontend state layer, and a decoupled backend. I like that I could give the ui to someone with only Typescript experience.
But in practice, it’s felt like every step (e.g., Auth0, and Stripe) is a bit unnatural and more complex than it should be (perhaps only because I am not a professional programmer). Despite the value in decoupling UI, the coordination between the component library and frontend — especially with props, forms, and state — can feel clunky. I’ve looked at tools like Biff, and part of me wonders why I didn’t just go with something like that: one project, one stack, less wiring.
So my question is:
Am I missing the point?
Did I drift too far from David Nolen’s advice on simplicity?
Is this kind of setup worthwhile, or just overkill for a small/hobby project (noting, I don't mind putting a little extra effort to get something that is easy and fun to use)?
Would love to hear if others have tried something similar or found better ways to manage this sort of split architecture.Did I drift too far from David Nolen’s advice on simplicity?It feels like on the JS-CLJS "spectrum" you have landed way too far on the JS side.
It's been quite some time since I watched that talk by David, but IIRC the UI components in their case were "atomic". Maybe something with complex behavior, but only if that behavior does not leak outside the component (like e.g. an auto-completing text input field). But mostly just simple things like buttons, inputs, containers, etc. Definitely not something that would need Zod or react-hook-form or full DnD handling.
Regarding the quoted code - there should be no need for clj->js at all. Check out how UIx interops with React components - it doesn't use an explicit conversion and it keeps prop names kebab-cased. There might be a need to convert something in some specific place, probably due to HOC, but it's definitely not something that has to be done everywhere.
Also, # in keywords is technically illegal.
> As a solo dev, I sometimes wonder if I’m just making things unnecessarily hard for myself.
That was my first thought while I was reading your message. :)
> There’s a strong appeal in the separation of concerns — having a clean, stateless UI library, a structured frontend state layer, and a decoupled backend.
Try to weigh your options more holistically.
If you love ice cream and only think about its great taste, you're unlikely to be able to keep eating it for long.
Same with any such principle as SoC.
> part of me wonders why I didn’t just go with something like that: one project, one stack, less wiring.
I'm wondering the same thing, to be honest.
But it doesn't contradict some separation. If you keep UI components very small, you can land on a sweet spot.
But that spot is sweet only if you really have to work with TS devs. If you're solo, then there's no reason to do it - just keep as much as feasible in CLJS.
> Is this kind of setup worthwhile, or just overkill for a small/hobby project [...]?
What does "worth" mean in this context? If your goal was to also tinker with 15 things at the same time, it was probably worth it. :)
But if the real need is to just create something that's, as you said, easy and fun to use, then you don't need a lot. Just keep things as simple as you can and not simpler. E.g. there's a chance you don't even need Tailwind and some rudimentary hand-rolled CSS or something like a static Bootstrap file will be just fine.
I'd second that this seems like overkill. For me the main reason I continue using Clojure(Script) almost exclusively is that I only have "one" language. One way of thinking, one way to do about things (e.g. data oriented). I can fall back to Java(Script) when I need to, but that is very rare. Doing that frequently would hurt my brain too much. CLJS is quite capable and can interface with most React libs just fine. I think David's example was in the context of a Team of developers that mostly knew TS/JS and this was easier to onboard them. Not so relevant as a solo dev.
There is however the AI argument. I think that is still completely horrible for CLJS, for the stuff I do at least. It seems to be somewhat reasonable for more "common" stuff though. So, maybe more common pure JS/TS based frontend with CLJ backend might make sense as a start. Can always switch to CLJS when you are more comfortable with CLJ.
I agree that building front ends in multiple languages is unnecessary complication. In my experience a team of engineers coming from JS world is perfectly capable to pickup cljs/react, that’s the reason why UIx is a 1to1 wrapper for React, I went this route with it because we had (and hired later) a number of folks with JS background who already knew React. If you are building solo, for leaning purpose, I’d also recommend to rebuild your project with other tech. There’s HTMX for simple frontends and Replicant — pure cljs alternative to React.
Thanks for your comments on this. Thanks for picking up on not needing cljs->js The conclusion is: it’s overkill for me. I’ll definitely be trying simpler stacks (e.g., Biff) on other projects. That said, I’d like to still defend it a little. Configuring has obviously been terrible, and thinking between two languages sucks, but there has been some good too. I actually learnt cljs before typescript, so this video got me to learn it. Storybook I’ve enjoyed using Storybook to understand and play with my components. In the talk, one of the reasons he uses JS is because of Storybook, which he recommends very highly. You can use it with CLJS, but maybe easier doing it natively. Using shadcn/ui Being solo, I don’t have time to write great components from scratch, so ising Shacn/ui has been helpful. It has been great to just slap in—it comes with accessibility baked in. I’m not sure how I could use shadcn/ui directly in CLJS. Forms When you said I landed too far on the JS side—do you think I could’ve done more form work in CLJS? Could I have just used input containers and handled validation in CLJS? Drag and Drop I just wouldn’t know how to do drag and drop without something like pragmatic dnd. I did try it with cljs and re-frame and was pretty bad. It was a feature I really wanted. In general, having access to libraries and using them in their native environment feels useful. Good to benefit from the javascript stuff. I mean there are limitations in using javascript/typscript libraries from uix2? I’ve accepted that this approach was hard for me being solo on it. I’m doing this on the side. I rely on expert advice, and David is a voice that makes a lot of sense, obviously being the maintainer of CLJS. So perhaps a reframing. How could I have best followed the advice of his video. Do you see validity in his points? Or arguments again it. There are defintely a few choices in making a web app, haha.
Oh, you can use NPM deps in cljs perfectly fine, including react libraries. That’s a norm in cljs projects. Storybook is great indeed, there’s also an alternative in cljs world — portfolio
> Storybook I haven't used it myself but there's at least https://github.com/nubank/workspaces - like Storybook but for CLJS. > I don’t have time to write great components from scratch, so ising Shacn/ui has been helpful You can use any JS UI library via interop. Just a bare minimum of JS knowledge is required. There are also some CLJS UI libraries and wrappers for JS UI libraries. > I’m not sure how I could use shadcn/ui directly in CLJS. Just as an idea - for each component, you could ask an LLM to convert it to CLJS for you. But no idea how reliable it would be. Could be not worth it at all, could be worth it only for some components. > do you think I could’ve done more form work in CLJS? Could I have just used input containers and handled validation in CLJS? Yes and yes. > Drag and Drop I just wouldn’t know how to do drag and drop without something like pragmatic dnd. I did try it with cljs and re-frame and was pretty bad. It was a feature I really wanted. DnD at the bare minimum is just a bunch of calls to a few functions in the HTML5 API. If you need more than just dragging a specific item to a specific different item or dragging a file onto a div, then using some thirdparty library makes sense. But it's still relatively trivial to do interop with it. > Good to benefit from the javascript stuff. You should use libraries where appropriate. But you don't have to have a separate JS build for that. > I rely on expert advice, and David is a voice that makes a lot of sense, obviously being the maintainer of CLJS. Keep in mind that David made that talk at a specific point in time, in specific circumstances. His circumstances might not be applicable to your case at all. They are certainly not applicable to mine, so I don't use that separation.
I don't even use react, so ask 10 people and probably get 11 different expert opinions 😉 All choices basically work, its best to pick one and commit to it. The biggest problem in today's world is that there is too much choice and getting bogged down and switching constantly because of FOMO or whatever. Everything will have trade-offs in some way. Using something popular (e.g. react) is always the safe choice. Just don't get locked into something a PaaS "vendor" tries to sell you, e.g. next.js
Thanks for all the helpful comments!!
Hi Folks, my project relies quite heavily on cljs-time currently. Now i’ve upgraded to the latest version, it finally breaks, because i think googles closure lib had some functions removed (eventhough for a few years now as it seems)… Anyone knows if there’s a fork or a replacement for it?
hrm can you be more specific? We manage Google Closure Library now, we could roll those change back
Hi David! well in my case, due to a transitive dep on an old clojurescript (i think), this function in cljs-time.core tripped up:
(defn now
"Returns a DateTime for the current instant in the UTC time zone."
[]
(doto (goog.date.UtcDateTime.) (.setTime (*ms-fn*))))but i was planning to remove our dependency on cljs-time … so i went ahead and got rid of it, so for me it’s solved now.
The one thing i did was: • upgrade clojurescript dep from 1.11 something to latest • upgrade shadow-cljs from 2.8.x to latest 3.0.5, which now requires com.google.javascript/closure-compiler-unshaded {:mvn/version “v20250407”} Maybe that had something to do with it.
ok interesting
@dnolen We also have a very deep dependency on the cljs-time lib and supporting Google Closure it depends on. So we would very much like to avoid any pain this space.
@denisj did you investigate what changed?
Breaking cljs-time is really undesirable
It usage is pretty pervasive - I’ve actually been restoring a lot of needlessly broken things in GCL - so now would be a good time to figure this out :)
Agreed. I have not actually experienced the breakage or had time to investigate differences because for other reasons we're still on older versions i.e.
[org.clojure/clojure "1.11.1"]
[org.clojure/clojurescript "1.11.132"]
[thheller/shadow-cljs "2.28.3"]
But I saw this thread and it got my attention. I don't want to have issue when we can move forwardjust tried cljs.time just now w/ the latest CLJS and GCL builds - (cljs.time.core/now) works just fine for me
Thanks, I'll sleep tonight
now that we're in control of GCL this kind of stuff will come to an end 🙂
fwiw, i’ve had great help with ChatGPT, in generating a drop-in replacement for cljs-time, wrapping Day.js At least for my purposes it eased my way out of cljs-time.
so if somebody needs that i could make it a small library, or just send you the few pages of code 🙂