Why call Hiccup from JRuby
Hiccup-style HTML is attractive because it represents markup as data: nested vectors, maps, strings, and symbols. From JRuby, that can be useful when a Ruby application wants a small rendering island that follows Clojure conventions.
The goal should not be to turn a Rails app into a Clojure app by stealth. The useful case is narrower: a template-free renderer for a well-bounded piece of HTML, shared with existing Clojure code or built around a data structure that Hiccup already expresses cleanly.
JRuby makes the experiment realistic because Ruby and Clojure can both live on the JVM and call across Java interop boundaries.
Keep the boundary small
The integration should have one obvious entry point. Ruby hands over plain data. Clojure returns a string or a small result object. Avoid passing rich Rails objects, open database connections, request objects, or framework-specific state across the boundary.
That rule keeps ownership clear. Ruby owns routing, persistence, and application flow. Clojure owns the rendering function. The data passed between them should be boring enough to print, test, and fixture.
A small boundary also makes failure cheaper. If the Clojure side raises, the Ruby caller can wrap the error with context and fall back or fail cleanly.
The data shape
The practical design starts with the HTML structure, not with interop mechanics. Define a simple Ruby hash or array shape for the content to render. Convert that into the Clojure data expected by the renderer. Keep attribute names, optional fields, and escaping behavior explicit.
Hiccup's advantage is that HTML becomes inspectable before it becomes a string. That is useful for programmatic generation, but it also means the data contract matters. A missing class name, unexpected nil, or unescaped string should be handled deliberately.
Tests should verify both the data passed to the renderer and the final HTML. Otherwise the interop layer becomes a place where each language assumes the other one checked the important parts.
Packaging and runtime cost
The JVM makes cross-language calls possible, but not free. Application boot, dependency loading, classpath setup, and version conflicts all become part of the deployment story.
For a small script or internal tool, that cost may be fine. For a production web request, it needs measurement. If every request crosses a dynamic boundary and builds large intermediate structures, the renderer may become slower than a conventional Ruby template.
The safe approach is to initialize dependencies at boot, keep the rendering function pure, and benchmark realistic payloads. The integration should earn its place by simplifying a real problem, not by being novel.
When not to do it
Do not add JRuby-Clojure rendering only to avoid writing templates. Ruby already has mature rendering options. The Clojure boundary pays off when there is existing Clojure code, a shared data-rendering model, or a domain where Hiccup-style composition is simpler than a template layer.
It is also a poor fit when designers or Rails developers need to edit markup frequently. A data DSL can be elegant for programmers and opaque for everyone else.
JRuby can host this kind of boundary well, but the boundary should be narrow, explicit, and easy to remove if it stops carrying its weight.
Calling Hiccup from JRuby
The useful experiment is not to replace Rails views. It is to see how a Clojure data structure can describe HTML, and whether that representation is pleasant from Ruby.
(ns views.article
(:require [hiccup2.core :as h]))
(defn card [article]
(str
(h/html
[:article.card
[:h2 (:title article)]
[:p (:summary article)]])))
require "java"
require "clojure/java"
Clojure.require "views.article"
card = Clojure.var("views.article", "card")
html = card.invoke({ title: "Hello", summary: "Rendered through Hiccup" })
puts html
The boundary is the cost. Passing simple maps and strings is fine for an experiment. Letting two language runtimes share a complex rendering pipeline can make debugging and deployment harder than the HTML problem itself.
When the bridge pays off
The bridge pays off when the Clojure side already has valuable rendering functions or when the team wants to experiment with HTML as data without moving the whole application. It is harder to justify if the only goal is to avoid ERB syntax. A second runtime is a real operational dependency, not just a new helper method.
A good boundary passes plain values: strings, numbers, arrays, maps, and rendered HTML. A bad boundary passes framework objects, lazy sequences with hidden lifetime expectations, or callbacks into Rails internals. The simpler the data crossing the line, the easier the experiment is to undo.
The experiment is healthiest when it has an exit strategy. If the Hiccup layer stays small and valuable, keep it. If it starts requiring custom conventions around every view, ordinary Rails rendering may be the simpler long-term home.
That reversibility matters in a Rails application with Rails deployment expectations. The interop layer should stay small enough to explain during a production incident, where plain data boundaries are much easier to debug under pressure.
Sources
- JRuby documentation. https://www.jruby.org/documentation
- Hiccup project. https://github.com/weavejester/hiccup
- Clojure documentation, Java interop. https://clojure.org/reference/java_interop