ClojureScript Questions + Answers
A friend emailed me some questions related to ClojureScript for discussion. I thought these answers might be helpful in a public forum. -- C. Oakman, 27 Sep 2020
Do you serve your CLJS/html from CDN or from same domain as application server?
In general I serve static assets from the same domain as the application server. I think adoption of CDNs is a bit cargo-cult these days; it certainly makes sense for the large web players (Google, Facebook, etc) but for the vast majority of smaller applications I think it introduces more complexity than benefit.
I like to think about what happens when things fail:
- If my application server goes down, but the CDN is still working, is my application still usable for an end-user?
- If my application server is up, but the CDN is down, is my application still usable for an end-user?
Obviously the nature of your application will determine answers to those questions, but in my experience for most projects the answer is a clear "no" for either situation. If you are fortunate to have enough load that you do not want to share traffic between the application server and whatever is serving your static assets, it is easy enough to reverse proxy and/or load balance using nginx from the same domain.
If from cross origin, are you doing JWTs for APIs or something else?
If serving files from a different domain, I would be sure to use Subresource Integrity (even if I was responsible for the content of the "upstream" file). I am a little paranoid about serving files from a computer I do not control.
I think JWTs are fine; I am on a project working with them right now. They are a bit verbose / involved for what they are doing. Whether using JWT or not, I think all web applications benefit from the concepts of:
- session - "Are you the same browser as that other HTTP request?"
- user authentication - "Are you who you say you are?"
- authorization - "Do you have permission to do this thing?"
While related, these are ultimately different things. A lot of times sessions and authorization get combined with authentication. In my experience this is a common source of confusion and bugs in web applications.
If you use JWTs, what are you doing for security?
I am using Keycloak as an authentication server on a project right now. It's pretty great - I think I will use it again for other projects. I definitely recommend using something that already exists (or "social login" OAuth2) instead of rolling your own.
If you ever have the thought: "we need to write password reset", there is a good chance you should stop what are you doing and find an off-the-shelf authentication server / mechanism instead ;)
Are you working with NPM packages? If so, what are you doing to bundle them and how are you managing overall bundle size and code splitting?
I use (and like) shadow-cljs, which bundles NPM dependencies into the CLJS build.
In my experience a lot of "bundle size too large" problems are a smell about application scope. I think about boundaries a lot - ie: What does this piece of software need to do? Where does its scope of responsibility end? A pattern I see often are projects with huge SPAs that really should be three separate pages / projects + a common shared library. Often changing the boundaries of what an application or library is responsible for can inherently solve the "bundle size" problem.
However, sometimes this is unavoidable and you have to ship a lot of code to the end user. In that case I would use Code Splitting functionality from shadow-cljs and well-organized CLJS namespaces to draw the boundaries of what should be a module.
What are your thoughts on CI/CD and automated testing/deployment for cljs?
Continuous integration (CI) is great; I recommend it. In the past I have used Travis CI, but recently have been using GitHub Actions. I didn't have a great experience with CircleCI - but whatever works for your workflow.
I like clj-kondo as a code linter. It definitely helps code quality.
I don't have any strong opinions about unit testing frameworks. At Luminare we
build a node.js script with shadow-cljs
:node-test target that runs some
cljs.test tests during our CI. The code that is being tested mostly runs in
the browser instead of node.js for our users, but what is being tested is not
really platform-specific so the platform difference doesn't matter too much.
Sometimes I like to sprinkle asserts near a function as a form of testing (example here). This can sometimes be noisy in the codebase, but also demonstrates the usage of the function in context. I like it as a lightweight way to sanity-check small functions.
It is important that your CI process can be repeated locally and not just on the CI server. I see this often: teams write platform-specific code so that the only way they can run their test suite is by committing + pushing due to some special CI runtime environment requirement. I do not recommend this. Get it working locally first, then in the CI environment.
Continuous delivery (CD) is great too, but there are a few gotchas to watch out for. I highly recommend adopting Twelve-Factor App principles and creating distinct build, release, run steps. Too often I see these steps coupled with the CI process such that the only way a team can produce a build or update a production server is through a commit + push cycle. I have similar advice here as with testing and CI: make sure you can do these steps locally, then automate / integrate them into your team's desired workflow.
I like to store release artifacts on s3 buckets for posterity and a sanity-check
separation of the build, release, run steps mentioned above. Having a single
.jar file as a release artifact is a beautiful thing. Clojure + CLJS really
shines here if it fits with your stack. Otherwise, a Docker image works too.
I like systemd for running things on servers: it's simple and has consistent runtime behavior. I'm pretty skeptical of a lot of current cloud offerings in terms of longevity / support. I'm pretty confident that Linux and systemd will be around in 20 years; I don't have that same confidence in Kubernetes (for example).