Putting Hanami in the browser via WASM
by Paweł Świątkowski
23 Jun 2025
The other day I was loosely listening to a Code and the Coding Coders who Code it podcast episode with Vladimir Dementyev. He was talking about his work around putting Ruby on Rails into the browser to lower the entry barrier for folks who just want to “feel” it, but witohut going through the struggle of choosing a correct Ruby version manager, installing dependencies for gems with C extensions etc.
And I thought: Wow, great!
And I also thought: If it’s (almost) possible with Rails, it should be even more possible with a truly modular framework, such as Hanami.
Despite not knowing a thing about WASM, I decided to give it a go. I had my first working version running in about half an hour. Then the reality struck and I spent another 6 hours figuring stuff, reading code by Vladimir and his e-book and banging my head against my keyboard.
Fortunately, after all this, I had a working version of a Hanami action (with validations) in the browser. You can get the code here.
Since the WASM package is 50MB in size, it does not fit Cloudflare Pages limits and I have read too many stories about putting large files on Netlify. So I bought a very cheap VPS and put the demo there. Here it is (I hope it still works when you are reading this post).
Some details and thoughts
This is a bit unstructured feed of my thoughts and learnings from this experiment:
- Everything related to Ruby and WASM goes through the ruby.wasm repository, which is under Ruby official org on Github. It mainly (for me) contains a
ruby_wasm
tool, available via gem, which does all the heavy lifting. - However, things are not that simple when you need to have custom gems available in your WASM package. In my case the problem was with
Hanami::Utils
, which rely on BigDecimal at the time of writing this.BigDecimal
is one of the gems known to not compile to WASM due to… something. - To resolve the above, I had to resort to a trick mentioned by Vladimir in the podcast - provide a dummy implementation of BigDecimal instead.
- … and I also had to force WASM compiler to skip attempting to compile the gem by overriding its internals.
- I learned a lot from Vladimir’s e-book Ruby on Rails on WebAssembly and probably even more from the code he wrote. These are excellent resources if you want to learn about Ruby and WASM. As my example proves, you can get started in quite short time.
Some code snippets
This is perhaps the most important part - fetching the WASM package and initializing a virtual machine with our gems and shims:
const wasmResponse = await fetch(wasmUrl);
const wasmBuffer = await wasmResponse.arrayBuffer();
console.log("WASM buffer size:", wasmBuffer.byteLength);
const module = await WebAssembly.compile(wasmBuffer);
const { vm } = await DefaultRubyVM(module);
console.log("Custom WASM loaded successfully!");
vm.eval(`
$LOAD_PATH.unshift "/shims"
require "/bundle/setup"
require "hanami/controller"
module WasmApp
class Action < Hanami::Action
end
end
def run_action(cls, request)
action = cls.new
response = action.call(request)
response.body.first
end
`);
return vm;
And this is the code taking contents from the editor and running it, using the run_action
method defined above:
async function runRubyCode() {
result = null;
isRunning = true;
if (!vm) vm = await initVM();
const ruby = editor ? editor.getValue() : editorValue;
const serializedParams = serializeParams();
const paramsString = JSON.stringify(serializedParams);
try {
result = vm.eval(`
${ruby}
run_action(TestAction, ${paramsString})
`);
isRunning = false;
} catch (error) {
console.error("Execution failed:", error.message);
isRunning = false;
result = error.message;
}
}
This also provides very basic error handling and will put the error in the browser.
Finally, the builder script that assembles a WASM package to be used by frontend:
require "ruby_wasm"
require "ruby_wasm/packager"
require "ruby_wasm/cli"
require "fileutils"
RubyWasm::Packager::EXCLUDED_GEMS << 'bigdecimal'
cli = RubyWasm::CLI.new(stdout: $stdout, stderr: $stderr)
args = %W(build --ruby-version 3.4 -o ruby-web-temp.wasm --build-profile full --stdlib)
cli.run(args)
args = %w(pack ruby-web-temp.wasm --dir shims::/shims -o src/ruby-web.wasm)
cli.run(args)
FileUtils.rm "ruby-web-temp.wasm"
This is all I have. It was a fun experiment with a completely new (to me) technology. Apparently it’s not hard to start with, but there’s a lot of dragons and rough edges still. But I learned something new and it was worth it.