I must admit I don’t fully understand what challenges you are facing. So I’m going to do a brave/stupid thing and just generally talk into this space based on what I think I understand and one or two things I’ve tried before. I hope it’s useful.
Generally, UI’s and backends are treated as “separate” projects. The UI (often React or Vue) are kept in separate source control repositories, apart from the backend project. The issue with this is that when it comes to run time, the UI and the backend are actually the same software stack. Ideally even, you want your backend to host the static assets of the frontend (ie have ring serve the .js
, .html
& .css
files from resources). It’s not strictly speaking necessary to host the UI assets from the uberjar, but it makes the job of your devops team slightly easier.
Another thing is that I’ve found that the build tooling around frontend/javascript projects lives parallel to the build tooling of clojure projects. What I mean with this is that usually js projects build with npm
or yarn
(or shadow-cljs
) or something; there are usually tools like PostCSS involved, you mention webpack etc. These are not integrated with lein
or boot
(and I don’t think they should be). Embrace and extend as they say…
Additionally, I’ve encountered the pattern a few times where the javascript build tooling for development depends on an HTTP dev-time server, that takes care of loading the assets from disk, reloads the frames when the assets are updated (eg you edit the javascript or the css, the postcss --watch
-type processes kick off, new assets are generated. This dev-time HTTP server usually has a websocket or something that then notifies the UI and a reload happens. It’s super convenient.
So with all of this in mind, I’ve structured a few projects like below and it worked for me:
- I host my UI and backend code in the same source repo, because I can symlink the output folder of the js build process into my clojure resources. I then have an HTTP route in clj that knows to serve those assets from there.
- I structure my UI code to load a settings file from
/settings.json
or /settings.edn
. This settings file contains high-level configuration, but usually at least the UI logging level and the protocol, host & port of the backend “API” service, eg http://localhost:8000
. I create a static settings file for dev-time that contains the correct assumptions (port) about dev-time and then I have the clojure backend craft a settings file at runtime that points back to itself. Often one of the configuration values you need to give an HTTP service as the “public URL”. This is the value I put in the settings file served to the UI.
- As mentioned above, I symlink the javascript project’s “output” folder into the clojure project’s resources. Resources are not often loaded from here during dev time, because you will lose the auto-reload, jacked-in websocket type behaviour I described. During prod-build time though, those assets once minified and compressed can live comfortably in the uberjar’s resources and can be served from there efficiently at run-time.
This setup allows me to start the clojure repl and service, start the js tooling, have all the niceties of hot-reloading for dev on the UI (and on the backend…) and still have the benefits of a self-contained deployable for prod/run-time.
There are other benefits too. Usually business stakeholders realise (or communicate) too late about whitelabeling user-interfaces. This setup with the settings.json
file allows to statically deploy customized versions of the UI to separate UI domains, and have them all use the same backend.
Anyway - this is what came to mind when you asked about unifying frontend and backend work.