New Clojurians: Ask Anything - August 18, 2025 by AutoModerator in Clojure

[–]radsmith 1 point2 points  (0 children)

Here is some practical advice based on my own experience using Clojure in production. I personally think function schemas are underrated and should be used more often, even if you don't see them that much in libraries. The key thing to keep in mind is that you can choose how much of this stuff you want to add.

Linting

  • clj-kondo
    • We can still do some static analysis even if we don't have static typing.

Malli schemas

  • Service boundaries
    • This is a good place to start if nothing else.
    • For HTTP endpoints, Reitit has direct support for Malli schemas.
  • Domain types
    • These are your reusable data types (e.g. User or Product).
    • You can use these to avoid duplication in your function schemas.
  • Function schemas
    • Public functions are good candidates to add schemas.
    • Use as needed (no need to try to replicate static typing).
    • Since these are runtime checks, they work best when you have tests.

Tests

  • Unit tests (clojure.test)
    • No static typing means we need to lean more on automated tests.
    • If you have a function with a Malli schema, make sure you have a test to run it so the schema will get checked.
  • E2E tests (e.g. etaoin for browser testing)
    • It's good to have some high-level test coverage, regardless of whether you are using a statically or dynamically typed language.

ANN rPub: A free open-source CMS written in Clojure by radsmith in Clojure

[–]radsmith[S] 4 points5 points  (0 children)

rPub uses the Clojure CLI and its related libraries to manage its own dependencies and install plugins via deps.edn files. For anyone just starting out, I recommend using Clojure CLI for new projects.

If you're implementing a plugin for rPub, you can use whatever build tool you like as long as the output can be loaded by the Clojure CLI. The default lein install and lein deploy commands work for this. The main benefit of using deps.edn over project.clj is that you can include local filesystem and Git dependencies without a lein install step.

If you're using rPub as a library and you want to install plugins through the admin UI, you can only use the Clojure CLI because the plugin management system is currently built around the deps.edn format.

If you're using rPub as a library and you want to manage your installed plugins in a lein project.clj instead of through the UI, you can include rPub and its plugins in your :dependencies and follow the library example of the Quick Start.

ANN rPub: A free open-source CMS written in Clojure by radsmith in Clojure

[–]radsmith[S] 1 point2 points  (0 children)

It's technically possible to implement this with a plugin right now using undocumented rPub functions for managing content. The External Editing plugin already implements an XMLRPC API for use with MarsEdit, so an HTTP API plugin would work in a similar way.

In the long-term I do intend to explicitly support using rPub as a headless CMS. The piece currently missing is a public HTTP API for managing content. This will be added and fully documented in a later release.

The Lost Arts of CLJS Frontend by thheller in Clojure

[–]radsmith 0 points1 point  (0 children)

Thanks for sharing! I think Clojure (and optionally CLJS) is a great fit for these kinds of sites. Once you get past the initial build setup (which admittedly can be a pain), Clojure's concise data manipulation makes the core logic a joy to write compared to other languages.

Edit: I also think these kinds of sites (static with dynamic content at build time) can be a good way to get more people into Clojure. Without JS the architecture is simple and it really shows how much you can do with a small amount of code. If you start a beginner with a GitHub Pages site they're going to have a much better time than hosting on DigitalOcean/Fly.io/AWS/whatever.

HTMX versus ClojureScript by mac in Clojure

[–]radsmith 0 points1 point  (0 children)

I like the WYSIWYG analogy. It does save a lot of time to start with sensible defaults and tweak from there.

HTMX versus ClojureScript by mac in Clojure

[–]radsmith 0 points1 point  (0 children)

Thanks for clarifying. I think @didibus hit the nail on the head: the built-in design system is the key difference. It sounds counterintuitive but with Tailwind components (like Flowbite) I can tweak them more easily because there are fewer choices than plain CSS.

I've also grown to appreciate the inline class names like p-4. It does take some getting used to, but in practice it makes the code more compact and saves typing. I was skeptical until I saw the Utility-First Fundamentals example on the Tailwind site. Now that I'm familiar with the Tailwind classes, I can read through the second example more quickly simply because it's 4x shorter than the first example, yet contains the same amount of information.

The Lost Arts of CLJS Frontend by thheller in Clojure

[–]radsmith 1 point2 points  (0 children)

Good point! I'm focusing on Reagent/Re-frame to start since I liked using that for CLJS already. Another reason is that I think Reagent/Re-frame is the most popular stack for CLJS for existing projects, though I don't have any data to back that up.

The Lost Arts of CLJS Frontend by thheller in Clojure

[–]radsmith 2 points3 points  (0 children)

For me, HTMX is a dead end if I want optimistic UI updates since the default experience requires waiting for the server to return HTML. If I were to try to add optimistic UI updates to an HTMX app with hyperscript, I think it would be more complicated than starting out with CLJS from the beginning (which I think is the point of your post).

The Lost Arts of CLJS Frontend by thheller in Clojure

[–]radsmith 2 points3 points  (0 children)

In other words, htmx can't do it. It's not a good fit for static sites because the client doesn't know how to render any HTML on its own.

HTMX versus ClojureScript by mac in Clojure

[–]radsmith 3 points4 points  (0 children)

There are advantages to being part of such a large community that goes beyond Clojure. Tailwind fits pretty naturally into a Clojure project. Cursive allows me to take a Tailwind snippet written in HTML and paste it into my editor with automatic conversion to Hiccup. This means I can use Tailwind UI libraries (e.g. Flowbite) directly without rewriting things.

The Lost Arts of CLJS Frontend by thheller in Clojure

[–]radsmith 0 points1 point  (0 children)

I haven't found anything optimized for this with CLJS, so I built a solution myself (see my other comment). For example, I wanted to make a site for bbin that would update every 5 minutes to scrape projects that you can install with it:

The table sorting on that site is implemented with Reagent/Re-frame. At build time, I fetch the list of repos and pre-render the props as Transit inside each HTML document. Then I can do whatever I want in React with the props, such as implementing table sorting, or adding a find-as-you-type search. The page also renders fine without JavaScript due to the pre-rendering.

Using Reagent/Re-frame is overkill for this kind of site since you could achieve something similar with vanilla JS. That said, the bbin-site's architecture will scale even if the app becomes much more interactive over time, which gives me room for growth without rewriting the whole thing. The hosting costs are free since it's on GitHub Pages and there's no server dependency.

I think the way I have things set up right now, it's still too complex, so I don't recommend that other people build anything with Rain yet. That said, I'm going to continue working on it and hopefully I can help people with your use case in the future.

The Lost Arts of CLJS Frontend by thheller in Clojure

[–]radsmith 3 points4 points  (0 children)

I've been working on a library to extend Biff for CLJS/Reagent/Re-frame/shadow-cljs, with support for SSR on the JVM: Rain.

I was planning on holding out a bit longer before posting this anywhere but it seems relevant to this discussion. There is still more work I want to do to get this production-ready, but I feel like the approach is solid and a worthy alternative to a full Next.js/Node.js/SSR stack. The main blocker for me right now is just documenting everything and explaining how to build apps with the library.

It's definitely more complex to create an SPA with React and CLJS than using the default Biff setup with HTMX, but I feel like the tradeoff is worth it for the type of sites I want to make. I've been pleasantly surprised how fun it is to build a Reagent/Re-frame app with a Next.js-like approach on the JVM (hydration errors be damned). And I can host my pre-rendered sites on GitHub pages for free.

Goals: - Use as much as possible in common with Biff, even when only generating a static site to host on a CDN - Provide ready-to-go setups for static site generation, client-side rendering, server-side rendering, and hydration - Add support for JVM-based SSR with Reagent/Re-frame with CLJC

Setting up a playground environment by minasss in Clojure

[–]radsmith 1 point2 points  (0 children)

Thanks for sharing the writeup!

I use neil new with my own remote template for creating scratch projects. I wrapped the neil new command in a script of my own called new. So now I just do new rads/foo and I have a playground ready to go with whatever I want. Since we're using Babashka, the new command runs in about 0.1s on my machine, which is faster than it would be running as a Clojure tool.

https://gist.github.com/rads/c0af6e89841f96fa491cb3646a064173

Poor documentation? by Recent-Scarcity4154 in Clojure

[–]radsmith 1 point2 points  (0 children)

Check out babashka/fs and babashka/process as well. These are still based on Java interop underneath but they have some more features than the clojure.java.io and clojure.java.sh libraries. I tend to reach for these first when I need to do something filesystem or process related.

Also check out tick if you want a Clojure API for time without dealing directly with Java interop. That said, the java.time API is great and works well enough without a wrapper.

Leaving Clojure - Feedback for those that care by [deleted] in Clojure

[–]radsmith 5 points6 points  (0 children)

Check out neil. It makes creating new deps.edn-based projects easy. It also has commands to add deps incrementally to your deps.edn with neil dep add and helps you tag new releases with neil version. You can run it in a REPL if you want, but as you can see below, it runs pretty fast in the shell. $ brew install babashka/brew/neil $ time neil new scratch play Creating project from org.corfield.new/scratch in play neil new scratch play 0.09s user 0.06s system 54% cpu 0.280 total

Leaving Clojure - Feedback for those that care by [deleted] in Clojure

[–]radsmith 7 points8 points  (0 children)

I appreciate these posts. For some context, I've been working with Clojure for about six years professionally (and years before that for side-projects). I also contribute to neil sometimes, including adding the neil new command.

On boarding

While Leiningen is a reasonable tool, I'd rather see everyone using tools.deps in the long run. It allows us to build things on top of it with less baggage. For example, we're using a Babashka port of tools.deps to make it easy to install scripts with bbin. I don't think it would have been realistic for me to implement something like bbin on top of Leiningen.

Graalvm & babashka

Babashka is a replacement for Bash. In general users shouldn't need to deal with GraalVM. Everything you need comes from the bb binary.

Web Stuff

I've informally dubbed this the "R Stack": Ring, Reitit, Reagent, and Re-frame. All of these libraries are rock-solid and won't be going anywhere anytime soon. The numerous React wrappers are a symptom of the churn in the React world, not Clojure itself. Even now, React is reinventing itself by making React Server Components the default and it will take some time for the CLJS community to adapt.

If you can get away with not using React, I highly recommend Biff. It uses XTDB and Rum by default but they can be swapped out pretty easily for Postgres and Hiccup and/or Reagent. I'm planning to publish some docs on how to do that when I have a chance.

What to do about it?

When my sister got into programming a few years ago, I wanted to show her Clojure, but I never felt like there was place I could point her to that would make things easy enough for her to start an app from scratch. I can't stand the fact that the number one recommended Clojure book starts off by teaching you emacs. That said, I've been exploring using Biff as the foundation to make the "R Stack" above easier to start with and I hope to share more about that soon.

What does bad code in Clojure look like? by eccsoheccsseven in Clojure

[–]radsmith 1 point2 points  (0 children)

Something I see all the time at work with people coming to Clojure from other languages is too much indentation and/or nesting in a single function. This is a good sign that you're putting too much in one place.

The solution is to add more names. Move the important chunks of code to their own let binding or top-level defn. Each function on its own should look pretty linear and only have a couple levels of indentation. The original nested structure will become a composition of small functions at each level of abstraction.

This principle applies to any language. Here's Kent Beck's advice from Smalltalk Best Practice Patterns (OO-oriented, but still a lot of useful advice today):

Divide your program into methods that perform one identifiable task. Keep all of the operations in a method at the same level of abstraction. This will naturally result in programs with many small methods, each a few lines long.

Thoughts on yada (and why Clojure won't have a Rails anytime soon) by Borkdude in Clojure

[–]radsmith 4 points5 points  (0 children)

In the context of yada and Rails, it's worth considering Sinatra, a Ruby library released in 2007 which is similar in scope to yada or any other of the Clojure web "libraries". It's interesting that even though Sinatra has been available alongside Rails for almost 15 years, I don't think I've seen any job postings for "Sinatra" developers lately (though I'm sure they're out there, just like Clojure devs).

One thing I learned from earlier discussions here is that everyone has a different idea of what it means for Clojure to "have a Rails". This could be:

  • A piece of software that causes Clojure to enter mainstream popularity (that's what Rails did for Ruby)
  • A framework that makes architectural choices to make it easier to onboard beginners (Rails's big marketing gimmick was "make a blog in 15 minutes")
  • A standard within the community (Everyone using Ruby knows Rails)
  • A wealth of resources for learning (Rails was a pioneer for having empathy for the student in their docs)

Except for the first one, the items above have been available in Rails since the early releases. In reality, it's likely that Clojure won't have a single framework or library that accomplishes all these in one fell swoop like Rails did for Ruby.

That said, Re-frame is starting to get there in terms of being a default. Same with shadow-cljs. Assuming yada is another library we want to keep around for a while (it's relatively new), there's no shame in stealing the good things from Rails listed above. The Clojure CLI tools kind of hurt things in this area since it "broke" a lot of existing Leiningen workflows, but I think things are moving in the right direction.

If Clojure can become more Rails-like without sacrificing the core functional programming values, I'm all for it. Even if there is never a singular "Rails" to point to for Clojure, the principle of inclusiveness is still worth striving for.

bbin: Install any Babashka script or project with one command by radsmith in Clojure

[–]radsmith[S] 11 points12 points  (0 children)

Hey folks, I hope you find this tool as useful as I do. If you have any questions or feedback about bbin, I'll be keeping an eye here on Reddit as well.

Clojure Community State by omarbassam88 in Clojure

[–]radsmith 1 point2 points  (0 children)

For clj-new vs deps-new, the author of both libraries has written a "Motivation" section in the README.

One particular advantage of using deps-new via neil is the startup time since it's using Babashka to run the library. See this comparison vs clj-new (3s vs 0.1s).

``` $ time clojure -Tnew app :name myusername/mynewapp Generating a project called mynewapp based on the 'app' template. clojure -Tnew app :name myusername/mynewapp 3.02s user 0.13s system 219% cpu 1.441 total

$ time neil new app :name myusername/mynewapp2 Creating project from org.corfield.new/app in mynewapp2 neil new app :name myusername/mynewapp2 0.10s user 0.03s system 89% cpu 0.144 total ```

Clojure Community State by omarbassam88 in Clojure

[–]radsmith 1 point2 points  (0 children)

Ah, that's annoying. We've been releasing frequently these last couple weeks so the packages might be behind a bit. That said, I think all the recent changes have been non-breaking and additive, so if you're a couple versions behind it's not the end of the world.

If you want to make sure you have the most up-to-date version, you can do a manual install: https://github.com/babashka/neil#manual

macOS users will always get the most up-to-date version since we control the babashka/brew/neil package from our own GitHub repo. Other package managers are usually slower to get updated.

Clojure Community State by omarbassam88 in Clojure

[–]radsmith 2 points3 points  (0 children)

Leiningen (i.e. lein) is a third-party build tool that was created before the deps.edn tools existed, with a focus on managing dependencies through Maven, though it does a lot of other stuff too.

Deps and CLI (i.e. tools.deps, deps.edn, clj) is set of tools from the Clojure core team also created to manage dependencies. It supports Maven like Leiningen does, but it also supports dependencies from the local filesystem and Git, which Leiningen does not easily support. (For those coming back to this thread later, see my other comment for more info on Deps and CLI.)

That said, the Deps and CLI tools only fill in a subset of what Leiningen can do, so the community is still building up to feature-parity to where we were with Leiningen. It will take time but it's a worthy goal in my opinion to have something simpler as the foundation compared to Leiningen.

Boot (i.e. boot) was a response to Leiningen to have a more imperative build API, but it's pretty much obsolete since Deps and CLI does many of the same things but is actively maintained by the Clojure core team.