<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://ryanbigg.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://ryanbigg.com/" rel="alternate" type="text/html" /><updated>2026-03-15T20:43:04+00:00</updated><id>https://ryanbigg.com/feed.xml</id><title type="html">Ryan Bigg’s Blog</title><entry><title type="html">From Heroku to Render</title><link href="https://ryanbigg.com/2026/03/from-heroku-to-render" rel="alternate" type="text/html" title="From Heroku to Render" /><published>2026-03-15T00:00:00+00:00</published><updated>2026-03-15T00:00:00+00:00</updated><id>https://ryanbigg.com/2026/03/from-heroku-to-render</id><content type="html" xml:base="https://ryanbigg.com/2026/03/from-heroku-to-render"><![CDATA[<p>After <a href="https://www.heroku.com/blog/an-update-on-heroku/">the announcement that Heroku is entering maintenance mode</a>, I got wary of the one (1) application I host on there going down. It’s a Rails app for photos for my kid, which has almost 2,500 photos of her since before she was born until the modern day. I created the app to share these photos with my family back in Adelaide.</p>

<p>I was also listening to <a href="https://www.remoteruby.com/2260490/episodes/18827088-heroku-hosting-and-the-ai-era">this Remote Ruby episode</a> Saturday morning that convinced me to go looking abroad too.</p>

<p>For a Rails app that’s nearing ten years old (for a kid that’s nearing ten years old), it holds up pretty well. This weekend-gone I bumped the Ruby and Rails versions to modern ones without much fuss. The biggest thing in that upgrade was moving off Webpacker and switching to ESBuild, just as that’s my personal preference now. And I brought in Propshaft over Sprockets. That’s all just for the sprinkles of React + Sass I have in this application.</p>

<p>That done, I went and tried to sign up to <a href="https://fly.io">Fly</a> and got some errors saying my attempt had “Validation errors” but it wasn’t clear what that meant. Then I got into the dashboard and attempted to setup an app, and got confused on the instructions for connecting a Rails app to either Fly’s managed or unmanaged PostgreSQL. This was <em>after</em> I dockerised the application based on Fly’s advice.</p>

<p>That’s the thing about Heroku: I didn’t have to care about dockerising or what PostgreSQL I was using. I could <code>git push</code> and Heroku would handle the rest of the setup.</p>

<p>After Fly gave me the irrits on Saturday, I started afresh on Sunday with a go of <a href="https://render.com">Render</a>. Sign up succeeded without validation errors (big tick) and after pushing my now-Dockerised app over to GitHub’s private container registry, I was able to deploy the app to Render easily.</p>

<p>Next up was the database setup which is its own separate service under the same project. That was a few button presses away. Switching back over to the app to add a <code>DATABASE_URL</code> environment variable I was pleasantly surprised to see a “Datastore URL” as an option in the environment variable screen. This set up the database URL correctly first try without me having to copy each part (username, password, dbname) over one by one.</p>

<p>Then it was only a matter of dumping the database from Heroku with <code>heroku pg:backups</code> incantations, and then restoring it into Render with a straight <code>pg_restore</code>, and then the app was fully deployed.</p>

<p>DNS cutover was straightforward too. I took out the Heroku entries, pointed them to Render’s load balancer and setup a Custom Domain for the app. I removed the SSL certificates from the Heroku app. It took about half an hour for the app to switch over properly.</p>

<p>I’m sure I’m only just touching the edges of what Render can provide, with nothing in this app needing things like autoscaling, one-off jobs, etc.</p>

<p>This is breath of fresh air compared to my day job where it’s Terraform-this and AWS-that. Being able to deploy an application with a few straightforward button presses in a UI felt like magic. The AWS UI designers sure could learn a lot from the Render team.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[After the announcement that Heroku is entering maintenance mode, I got wary of the one (1) application I host on there going down. It’s a Rails app for photos for my kid, which has almost 2,500 photos of her since before she was born until the modern day. I created the app to share these photos with my family back in Adelaide.]]></summary></entry><entry><title type="html">Hearts &amp;amp; Clubs</title><link href="https://ryanbigg.com/2026/02/hearts-clubs" rel="alternate" type="text/html" title="Hearts &amp;amp; Clubs" /><published>2026-02-06T00:00:00+00:00</published><updated>2026-02-06T00:00:00+00:00</updated><id>https://ryanbigg.com/2026/02/hearts-clubs</id><content type="html" xml:base="https://ryanbigg.com/2026/02/hearts-clubs"><![CDATA[<p>At the end of May 2025, my partner (Shaz) and I went to buy a new house. We had inspected it a few times previously and loved it. It was the day of the auction, and we went out for a morning walk with the dog and my heart started doing a weird thing: it started beating really really fast. It sustained the pace for about 5-10 seconds, then returned to baseline. Then it would repeat this about every minute. I put it down to the stress of the auction.</p>

<p>We went to the auction and we were the only bidders, and got bumped up a few notches by vendor bids, and then settled on a final price. All through this time my heart was still doing its slow-fast-slow switching every minute or two. Still putting it down to stress. We sign the contract for the house and so on, and the heart is still doing its thing.</p>

<p>Sunday morning rolls around and it’s <em>still</em> going slow-fast-slow. I start up the heart rate app on my Apple Watch and clock a 160 while lying completely still in bed. Then a 170. I switch to the ECG and I see my heart <em>stopping</em> for about a second, then beating rapidly and then resuming a regular pace. A flat line is not the kind of line you want to see on an ECG.</p>

<p>This continues all through Sunday, and I’ve not let anyone know. Then I mention it to Shaz in the evening and I say I’m going to the Emergency Department. We have a good conversation about how I’m an idiot for not doing this sooner. I get in there, tell them about my condition and they hook me up to a heart monitor for a few minutes. It does not catch the condition. I stay there for approximately 6 hours, un-monitored, and then I’m told that I should be alright to go home. I get home at 2am.</p>

<p>Throughout the next week, it happens more than a dozen times every hour. It gets to Wednesday night and I’m at trivia with my friends, a few ciders deep and my heart is going off <em>every single trivia question</em>. That’s a quicker pace than it’s ever done. On Wednesday night I’m exhausted from trivia and the week in general, so resolve to go to the Emergency Department on Thursday morning. I put it off due to my nothing-burger of an experience on Sunday/Monday.</p>

<p>Thursday morning, I go into Emergency Department, let them know I was in Sunday (no, they don’t do “fifth visit is free!” loyalty cards) and they immediately hook me up to an ECG. Within seconds it’s going off its nut. The heart rate indicator flashes yellow briefly and it goes beep-beep-beep before it flashes a faster red colour and goes BLART-BLART-BLART. The heart rate reads at ~180bpm. I’m in a hospital bed. I only hit 180 on the regular towards the end of my workouts or on a tough bike ride.</p>

<p>They give me medicine called Metoprolol. It quietens the condition for a few hours, and it starts to kick off again just after dinner time. I get a photo of the machine reading 173, but that’s only because it changed when I went to capture the 193 it flashed up right before that. I’ve never seen a 193 during a workout.</p>

<p>I’m given another dose. It goes quiet again for another couple of hours, and kicks off again around 3am. The machine is doing its beep-beep BLART-BLART-BLART routine and I’m a light-sleeper at the best of times. It doesn’t help that this is a shared room and someone in the bed across the way is a worse snorer than me. I put some earbuds in and blast KGLW’s PetroDragonic Apocalypse on loop all night. It actually works and I get back to sleep.</p>

<p>The morning comes and I’m sent home with a referral to a cardiologist and a heart clinic and a prescription for the meds I was on. I <em>immediately</em> take that prescription and get the meds, taking two a day to keep the heart calm. They discharge me with a <em>suspected</em> condition called <a href="https://www.mayoclinic.org/diseases-conditions/supraventricular-tachycardia/symptoms-causes/syc-20355243">SVT</a>.</p>

<p>The referral to a heart clinic leads to me getting a Holter Monitor, not in Warrnambool but in Colac, a 3-hour return-trip away. At the end of June. The Holter Monitor is hooked up to me and I look part-cyborg. I wear it for 24 hours, during which no events happen … because I’m religiously taking the medicine the hospital prescribed.</p>

<p>The referral to the cardiologist leads to a phone call a month later while I’m in Melbourne. They say they’ve got a booking the next day and could I come in for that. I say I can’t make it because of a prior commitment and ask when their next available appointment is. They tell me the 3rd of February, 2026. Yes, 7 months later <em>for a heart condition</em>.</p>

<hr />

<p>That appointment rolled around just this week gone. I am now on my third “bottle” of prescription pills, thankfully only costing $13.75 for 100 pills (and I take a half-dose) because I live in a country with socialised medical care. A++ would recommend to others.</p>

<p>I drive the 3 hours to Geelong and arrive early at the clinic. When I get in to see the Cardiologist, I can see his computer screen. He pulls up my record and makes a face. There’s no referral data there, at all. Nothing from the hospital. Nothing from my GP. Nothing from the clinic. I end up showing him the ECG results I recorded on my Apple Watch and he hesitantly accepts those. He makes suggestions for what it could be, but is unable to diagnose the condition. A follow-up appointment gets booked, and they tell me they’ll <em>mail out a letter</em> like it’s eighteen-bloody-fourty-six.</p>

<p>On the 3 hour drive back, I have a long time to make enquiries as to who fucked up here. I call the hospital. I call the clinic. I call the GP. It is unclear who has what information, and I ask each to send me everything they have. I put on my <em>best</em> “Customer Service” voice and approach each call with the attitude of: “this person I’m speaking to is not responsible for this issue, they’re a messenger”.</p>

<hr />

<p>All last week and this week I’ve been troubleshooting work issues. This week I worked Monday, off Tuesday for Cardiologist, and worked Wednesday &amp; Thursday. We’re in the end-phase of a migration project that’s been going since April, cutting merchants over from one payment processing system to another. We acquired another company in April, and we’re porting all of their merchants over to our systems as best we can. This process is not without its issues, as we have well-and-truly discovered since almost day-dot.</p>

<p>Simultaneously to this, we have other parts of the business handling money-in and money-out for merchants. I learned very early in business that people are sensitive to money issues. And fair enough because money is oxygen for businesses, and money is how we as people survive in this world. What ends up happening is that the bank can be late in their payment, which makes us late in our payments. We’re talking millions of dollars here. Ultimately, everything reconciles out and all the money gets to where it needs to go. When there’s a hiccup, it can feel pretty stressful.</p>

<p>In the past, I have glibly remarked to others that when there’s been issues with money that: “at least nobody’s gonna die”. My viewpoint there is that yes, the job is stressful, but nobody’s going to die if the money takes an extra few hours to arrive. I’m trying to set perspective for myself and for others. It works (for me), but barely.</p>

<p>These work issues that I’ve been troubleshooting come through our support email and these emails can vary in description from “it no work good” all the way up to drawing a big red target on the issue and putting a neon sign abve it saying “right here, this one”. Such is the nature of any support line. I like the target issues because they’re easy, but I also like the “no work good” ones too, because I’m a sicko for a good puzzle.</p>

<p>In a lot of the cases of both, I’ve been helping triage those issues by reading through what the merchants have written and attempting to diagnose the issue. This isn’t always successful. In those cases, I’ve taken the liberty to do a radical thing and <em>call the customer on the phone</em>. Radical, I know. Then when that’s not worked, we’ve gotten on a video call and walked through the issue. Both of these have been immensely helpful to nail down some tricky issues pretty quickly, without having the messages bounce up and down the chain of me -&gt; support -&gt; customer and back again.</p>

<p>There’s been some pretty obvious bugs, including one which was a <code>LIKE</code> that should’ve been an <code>ILIKE</code> that I wrote about three months ago. Mea culpa.</p>

<p>There’s been some more nefarious ones like if these 8 interlocking things are all true but these other 3 things are false then we’ve gotta do a thing.</p>

<p>It’s been helpful to consult with the customers and walkthrough these issues with them and to work with some really awesome people on my team to further diagnose and fix these bugs in a timely manner. I think we ended up shipping over twenty changes over the week in response to these issues and other feedback. It was pretty productive despite being so short a week.</p>

<hr />

<p>Then we get some customers who are (rightfully) upset that the new system isn’t working the same way as the old system. The two aren’t one-to-one compatible because the new system is a re-implementation of things from mostly spoken-word generational-lore. It’s been a swell time. Many learning and development experiences were had. “If I was to have my time again” has been bandied around <em>a lot</em>.</p>

<p>These customers <em>demand</em> immediate rectification of the issues, lengthy explanations of what has happened, and guarantees that all attempts to use the system will be bug-free on an on-going basis. Anyone adjacent to software for any length of time knows full well that the rectifications aren’t going to be immediate, that the explanations will be patchy, and the software <em>will</em> contain bugs.</p>

<p>We listen to their concerns, make Jira cards about the issues and set about fixing them. We write up explanations of how the bugs can occur, and estimate how long it’s going to take them which, again, as we all know, is a precise art.</p>

<p>One “great” thing about payments is that in some cases we can’t know if something’s going to succeed in production until it’s attempted. So we let those customers know that the known bugs have been fixed, they re-attempt some payments, and a subset of those payments fail. We fix <em>those</em> bugs or tweak <em>those</em> configuration settings, and get them to try again. Understandably, people are hesitant to “test in production” when it involves real people and real money, but such is the nature of payments. This usually takes a few tries to iron out most issues.</p>

<p>As the issues are being ironed out, tempers flare as other bugs pop up further “down the road”. Those bugs get fixed as well. Payments are complicated. I reply back and say that the team is working as hard as they can on rectifying issues, which is the honest truth. It falls on deaf ears.</p>

<hr />

<p>Today, the follow up cardiologist letter arrives. The next available appointment is on the 18th of August. 1 year, 2 months and 2 weeks since my initial admission to hospital. I get quite angry about this and fortunately I’m home and nobody’s around to witness how I react to this news. I recover and end up taking the dog for an overdue walk to the park. She runs around like a doofus and brightens my day.</p>

<p>I come back from the park walk and start making some more phone calls. I call everyone I called on Tuesday and follow up the paper trail for my heart condition. I explain the situation calmly and with empathy, because I know these people aren’t the ones directly responsible. They’re just the messenger.</p>

<p>What sits in the back of my mind the whole time is this: “what if my heart failed during this call?”. I’m home alone today with nobody else except the dog and two cats. The other people in this house arrive back here in four hours time. If my heart <em>does</em> decide to go “bad”, will I have enough time to call emergency? That same question sits in my mind all day, every day.</p>

<p>In payments, at least nobody is going to die. But with this heart condition and this massive delay on treatment (14 months and counting), maybe somebody <em>is</em> going to die – me. And maybe it could be prevented by someone doing something as simple as sending an email or a fax, and having the process expedited by that.</p>

<p>I intend to find out what records people have about my stay in hospital and visits to the various clinics. I intend to get paper evidence of all of this. And I intend to do this in a way that is <em>not</em> demanding immediate rectification, lengthy explanations of what happens and guarantees of a flawless system. Because I know that people are <em>falliable</em>  and make mistakes. I will choose in this situation to use my heart instead of my club.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[At the end of May 2025, my partner (Shaz) and I went to buy a new house. We had inspected it a few times previously and loved it. It was the day of the auction, and we went out for a morning walk with the dog and my heart started doing a weird thing: it started beating really really fast. It sustained the pace for about 5-10 seconds, then returned to baseline. Then it would repeat this about every minute. I put it down to the stress of the auction.]]></summary></entry><entry><title type="html">Beware grpc gem and Ruby 4.0</title><link href="https://ryanbigg.com/2026/01/beware-grpc-gem-and-ruby-40" rel="alternate" type="text/html" title="Beware grpc gem and Ruby 4.0" /><published>2026-01-19T00:00:00+00:00</published><updated>2026-01-19T00:00:00+00:00</updated><id>https://ryanbigg.com/2026/01/beware-grpc-gem-and-ruby-40</id><content type="html" xml:base="https://ryanbigg.com/2026/01/beware-grpc-gem-and-ruby-40"><![CDATA[<p>Finally got to the bottom of ridiculously slow build times on one of my applications. I’m talking 30+ minute builds, all without <code>sassc</code>!</p>

<p>We use a gem in the app called <code>newrelic-infinite_tracing</code>, which has a dependency on another gem called <code>grpc</code>. This gem has native extensions that are pre-built. You’ll see these listed on RubyGems as lists like:</p>

<ul>
  <li>1.76.0 October 24, 2025 x86-linux-gnu (22.5 MB)</li>
  <li>1.76.0 October 24, 2025 x86_64-linux-gnu (19.8 MB)</li>
  <li>1.76.0 October 24, 2025 x86_64-linux-musl (18.8 MB)</li>
  <li>…</li>
</ul>

<p>These list the version, architecture and platform that you’re going to be installing these gems on. These gems can also be locked to specific Ruby versions, and these 1.76.0 gems are indeed locked to only Ruby <code>&gt;= 3.1</code> and <code>&lt;= 3.5.dev</code>. <strong>This does not include Ruby 4.0!</strong> So when we go to install this gem onto Ruby 4.0, it finds <em>no</em> precompiled binaries, and instead compiles it all from scratch, bringing back memories of <code>sassc</code> and <code>nokogiri</code>’s old compile times before RubyGems introduced this wonderful precompiled binaries feature.</p>

<p>I got to the bottom of this issue by running <code>bundle install</code> with no more <code>-j</code> option, and then measuring which gem took the longest time to install during a CI build step. The step helpfully output timestamps on each line of the <code>bundle install</code> process, which helped a lot toward narrowing it down!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Finally got to the bottom of ridiculously slow build times on one of my applications. I’m talking 30+ minute builds, all without sassc!]]></summary></entry><entry><title type="html">Triaging bugs</title><link href="https://ryanbigg.com/2025/11/triaging-bugs" rel="alternate" type="text/html" title="Triaging bugs" /><published>2025-11-30T00:00:00+00:00</published><updated>2025-11-30T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/11/triaging-bugs</id><content type="html" xml:base="https://ryanbigg.com/2025/11/triaging-bugs"><![CDATA[<p>At Fat Zebra, one of my duties as a team lead is managing the workloads of those I work with and falling into that ambit is bug triaging. We have a dedicated support channel where people can tag all leads and then the responsible leads can triage those issues. All leads get tagged as it’s sometimes unclear who is responsible for an issue, and it helps with the “pinball effect” that can go on for tickets in their early stages.</p>

<p>Another rule of thumb is that when I can see a ticket is about my team’s work is that I’ll assign it to the on-call person for the team to investigate. This helps spread the load away from myself, and trains up the rest of the team on how to investigate all sorts of issues. Other people may be roped into help investigate if the issues lies in their area of expertise.</p>

<p>My team came up with this list of triage questions to ask and posted about it in our internal wiki. We train people who interact with our team on this triaging method. We heavily encourage all work to be logged in a ticket, so that we get a general idea of how much time has been taken up by this triaging process or “BAU” and how much has been taken up by features.</p>

<p>The questions we want answered in the tickets are these:</p>

<ol>
  <li><strong>Which merchant is having this issue?</strong> Who is the issue affecting? Are they are one of our larger merchants or a smaller merchant? Or is it more than one merchant reporting this issue?</li>
  <li><strong>What is the scope of the issue?</strong> At a rough guess, what % of this merchant’s functionality is degraded? For example if it’s a transactional issue, is it an issue with one type of transaction (such as Apple Pay) or is it across the board?</li>
  <li><strong>Where can we see the issue happening?</strong> A URL to the site of the issue is incredibly useful here.</li>
  <li><strong>Can you demonstrate the issue?</strong> Can you send us a video of the issue and walk us through your thinking on this. Use Loom. Post the video in the team channel.</li>
  <li><strong>If you can’t send a link or demonstrate, can you describe the issue in a few sentences?</strong> Using your words to explain an issue over saying something like “purchases aren’t working” really helps us get to the root cause of an issue sooner. The more words the better.</li>
  <li><strong>From your perspective, how urgent is this issue?</strong> Do we need to be waking people up about this if it’s occurring at night, or can it wait until the morning? Could it even wait until the next Sprint?</li>
</ol>

<p>We then provide a template for them to use when creating a ticket for our board:</p>

<blockquote>
  <pre><code>**Merchant Affected:** [Merchant name]
**Scope:** [% of functionality impacted, or specific features impacted]
**Steps to Reproduce:** [Link to URL of the affected page or video walkthrough]
**Urgency:** [Low, Medium, High - based on business impact]
</code></pre>
</blockquote>

<p>We then go on to say:</p>

<blockquote>
  <p>Tickets without enough information will be re-assigned back to the reporter.</p>

  <p>When you’ve created the ticket with this information, post it in the #cxteam channel on Slack.</p>

  <p>Do not @here in #cxteam, as there are usually upwards of 20 people who will receive your message.</p>

  <p>In an urgent situation, escalate through Slack with to the person currently on call with:</p>

  <p>[on call alerting instructions go here]</p>
</blockquote>

<p>This has really helped reduce the noise that goes on when a ticket rolls in. It can be a bit frantic to start out with; a very “my hair is on fire” moment. This happens because the downstream merchant has been upset about an issue, and then that escalates up through the chain until it reaches the triage point. At that point, we determine the answers to the questions above and act accordingly. We haven’t yet gone onto classify these based on something like a <a href="https://www.productplan.com/glossary/rice-scoring-model/">RICE</a> score, but I think it would be helpful, at least the Reach + Impact parts of that.</p>

<p>The response between each ticket varies tremendously. Sometimes they don’t get past the first couple of people, and sometimes they involve multiple teams worth of effort over a couple of days. It’s important to figure out the scope of these issues at the very start, so that we can be sure that we’re addressing the important or urgent issues first and we don’t get overwhelmed by the noise.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[At Fat Zebra, one of my duties as a team lead is managing the workloads of those I work with and falling into that ambit is bug triaging. We have a dedicated support channel where people can tag all leads and then the responsible leads can triage those issues. All leads get tagged as it’s sometimes unclear who is responsible for an issue, and it helps with the “pinball effect” that can go on for tickets in their early stages.]]></summary></entry><entry><title type="html">Ruby Community Reflections</title><link href="https://ryanbigg.com/2025/10/ruby-community-reflections" rel="alternate" type="text/html" title="Ruby Community Reflections" /><published>2025-10-29T00:00:00+00:00</published><updated>2025-10-29T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/10/ruby-community-reflections</id><content type="html" xml:base="https://ryanbigg.com/2025/10/ruby-community-reflections"><![CDATA[<p><strong>Content warning: suicide</strong></p>

<p>This year, we ran another <a href="https://ryanbigg.com/2024/10/ruby-retreat-2024">Ruby Retreat</a> with 50 people in attendance. This event shows off how good the Ruby community in Australia is by gathering people together from the Friday afternoon until the Monday morning. I’d say that this event was a success again.</p>

<p>At the start of the event, I got up and had this to say:</p>

<blockquote>
  <p>DHH wrote a long blog post about how, essentially, there aren’t enough white people in London anymore and how white folk have to rise up. I won’t mince words here: He went full mask-off racist. Those views are abhorrent and have no place in a modern society. They lead down a dangerous path. We cannot be tolerant of the intolerant.  The philosopher Karl Popper called this the paradox of tolerance — that a tolerant society cannot survive if it tolerates intolerance. If we allow bigotry and exclusion to stand unchallenged, they will eventually silence the very openness that makes our community strong.</p>

  <p>I encourage you to find your voices and stand up against this intolerance whenever you see it in our community. Intolerance and division have no place in our community.</p>

  <p>I wanted to run this Ruby Retreat because these events have exemplified the kind of community and community event I want to see more of in the developer space. These events, and those attending, have been an exact antithesis to what DHH is preaching. We are stronger together, than we would ever be split apart into different tribes.</p>

  <p>I want these events to exist so that we can show off the great parts of the Ruby community. These events are what makes me love Ruby so much.</p>

  <p>As our Code of Conduct says:</p>

  <blockquote>
    <p>Whenever we come together as a community, our shared spaces are opportunities to showcase the best of what we can be. We are there to support our peers - to build each other up, to accept each other for who they are, and to encourage each other to become the people they want to be.</p>
  </blockquote>

  <p>So as we gather here this weekend, let’s remember that the Ruby community is only as good as we make it — together. Inclusivity isn’t a one-and-done checkbox; it’s a practice. It’s in how we welcome new voices, how we disagree respectfully, and how we draw clear lines around what we will not accept. Societies have been doing this for centuries — it’s why we have laws.</p>

  <p>Events like this show us the best version of what Ruby can be: creative, kind to all, and committed to lifting everyone up. Let’s take that attitude into this weekend, and beyond.</p>
</blockquote>

<p>We saw strong evidence of this during the camp with communal lunch and dinner times, and people splitting into different groups to work on different projects, or play games like Codewords or Go. And yes, this time there was even more Blood on the Clocktower too.</p>

<p>One of the people present at the Retreat was a woman called Caroline Bambrick.</p>

<p>I knew Caroline, or Caz, through working with her during the Junior Engineering Program #2 at Culture Amp. She wowed the interviewers with her skills and got to be chosen as one of the nine people we ended up picking. While she had that common anxiety of a new starter (“omg they’re going to fire me the moment I mess up”), she ended up being a critical part of that group.</p>

<p>Of course, lives take different directions. I was made redundant and then Covid hit, and so we all drifted apart. I’m also remarkably bad at keeping in touch with people I would call friends.</p>

<p>Caroline attended both last year’s Ruby Retreat and this year’s. My only photo of her from this year’s event is of her being her extremely-picky-but-charming self, trying to best optimise the best way to stack her lunch plate to get a bit of everything and not to miss out on anything. I reckon she took about two minutes at the front of the line.</p>

<p>She played Codewords and laughed along with people when the game went sideways as clues were misinterpreted.</p>

<p>She was there for Blood on the Clocktower, where she played the role of the Scarlet Woman so <em>utterly flawlessly</em> it fooled us all.</p>

<p>She was, as best anyone could tell, another face in a crowd of 50 people.</p>

<p>By the following Wednesday, two days after the event, she had chosen to end her life.</p>

<p>The news was shared on the Ruby AU Slack this Monday morning, with over 100 broken heart reactions on that thread. The thread is full-up of stories of how Caz has impacted people’s lives for the better, and photos of her time in the Ruby community.</p>

<p>Her funeral was today, and a group of Australian Rubyists organised to turn up together. Quotes of her from the Ruby AU community were shared by community members Lauren, Pat and Brendan. Hugs and condolences were shared all around. I cried.</p>

<p>I got to talk to Caz’s mum about how she made me a better manager and a better <em>person</em>.</p>

<p>All of this is the kind of support I meant in my Ruby Retreat message. I just wish we could’ve all given this support sooner and <em>somehow</em> prevented this tragedy.</p>

<p>My head kept trying to problem-solve its way out of this horrible situation last night as a way of coping with this trauma, periodically waking me up to signal that it hadn’t yet solved the problem, but by golly it was gonna work its hardest on it. The problem isn’t solvable; the conclusion is, sadly, final.</p>

<p>Tonight, we had the Melbourne Ruby meetup as well. There were talks on database sharding and PostgreSQL tablespaces. Many of the attendees of the funeral were there too, but there were also some new faces who had only been attending the meetup this year. The Ruby community is still thriving in Melbourne.</p>

<p>After the meetup, we went out for ice cream at Pidapipo, just a short walk over into Degraves Street. There were more hugs. We took a group photo, that had a lot of the people from the meetup in it. But there will forever be a hole in our community. We have lost a strong advocate for not only the Ruby community, but humanity in general.</p>

<hr />

<p>As was stated on that Ruby AU thread: Suicide is a very hard topic for a lot of people, please don’t suffer in silence. If you, or someone you know needs support or help, please contact:</p>

<ul>
  <li><a href="https://www.lifeline.org.au/">Lifeline</a> provides 24-hour crisis counselling, support groups and suicide prevention services. Call 13 11 14, text 0477 13 11 14 or chat online.</li>
  <li><a href="https://www.suicidecallbackservice.org.au/">Suicide Call Back Service</a> provides 24/7 support if you or someone you know is feeling suicidal. Call 1300 659 467.</li>
  <li><a href="https://www.beyondblue.org.au/">Beyond Blue</a> aims to increase awareness of depression and anxiety and reduce stigma. If you or a loved one need help, you can call 1300 22 4636, 24 hours/7 days a week or chat online.</li>
  <li><a href="https://www.bigfeels.club/">Big Feels Club</a> provides shared stories and experiences for people who have done ‘all the right things’ but still feel stuck.</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Content warning: suicide]]></summary></entry><entry><title type="html">Hanami for Rails Developers: Part 4: Associations</title><link href="https://ryanbigg.com/2025/10/hanami-for-rails-developers-4-associations" rel="alternate" type="text/html" title="Hanami for Rails Developers: Part 4: Associations" /><published>2025-10-13T00:00:00+00:00</published><updated>2025-10-13T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/10/hanami-for-rails-developers-4-associations</id><content type="html" xml:base="https://ryanbigg.com/2025/10/hanami-for-rails-developers-4-associations"><![CDATA[<ul>
  <li>Part 1: <a href="/2025/10/hanami-for-rails-developers-1-models">Models</a></li>
  <li>Part 2: <a href="/2025/10/hanami-for-rails-developers-2-controllers">Controllers</a></li>
  <li>Part 3: <a href="/2025/10/hanami-for-rails-developers-3-forms">Forms</a></li>
  <li>Part 4: <a href="/2025/10/hanami-for-rails-developers-4-associations">Associations</a> (you are here)</li>
</ul>

<p>In the first three parts of this guide, we set about building up a way that works with a table called <code>books</code> to display these records through some controller actions, and to allow us to create more and edit them in forms.</p>

<p>In this part, we’re going to cover how we can set up an association to books called <code>reviews</code>. We’ll create a new table for this, and work out how to display reviews next to books on the <code>books.show</code> page. In this part, we’ll be spending a lot of time working back on our repositories and relations.</p>

<h3 id="creating-the-table">Creating the table</h3>

<p>To get started, we first need to create a table called <code>reviews</code>. We can do this by generating a migration:</p>

<pre><code>hanami g migration create_reviews
</code></pre>

<p>In that new migration under <code>config/db/migrate</code>, we’ll change the code in that new file to create this new table:</p>

<pre><code class="language-rb">ROM::SQL.migration do
  change do
    create_table :reviews do
      primary_key :id
      foreign_key :book_id, :books, null: false, on_delete: :cascade
      String :content, null: false
      Integer :rating, null: false
      DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
    end
  end
end
</code></pre>

<p>This table will have all the columns you’d expect to have for a review, minus a user association. We don’t want to get too carried away at the moment!</p>

<p>We can run this migration with:</p>

<pre><code>hanami db migrate
</code></pre>

<h3 id="review-relation">Review relation</h3>

<p>Next, we need to create the classes within our application that we’ll use to manage these records in the table. The first of these that we’ll need is a relation so that we can query that table. We’ll generate one with this command:</p>

<pre><code>hanami g relation reviews
</code></pre>

<p>Let’s see how we can create a new review with this relation by booting into the console:</p>

<pre><code>hanami console
</code></pre>

<p>Once we’re in this console, we will load the relation with:</p>

<pre><code class="language-ruby">reviews = app["relations.reviews"]
</code></pre>

<p>To insert a new review, we’ll run this code:</p>

<pre><code class="language-ruby">reviews.insert(
  book_id: 1,
  content: "I now finally understand Hanami!",
  rating: 5
)
</code></pre>

<p>This’ll return simply <code>1</code>, indicating the ID of the record that we saved.</p>

<p>Now how would we return the reviews for a book? Well, we can simply ask for them:</p>

<pre><code class="language-ruby">reviews.where(book_id: 1).to_a
</code></pre>

<p>However, we’re going to want to display these reviews on a book’s page eventually. In a Rails app it would be a simple matter of <code>book.reviews</code>. However in a Hanami application, the <code>book</code> object in question would be a simple struct with no association methods defined on it. This is by design, to remove a very large footgun in the shape of N+1 queries that are a bugbear of any Rails developer. In a Hanami application, it is impossible to do an N+1 query.</p>

<h3 id="loading-a-book-and-its-reviews">Loading a book and its reviews</h3>

<p>Hanami has a way of loading both the book <em>and</em> its reviews together. We’re now going to set this up, by first defining an association between books and reviews over in <code>app/relations/books.rb</code>. We define associations in Hanami by changing the <code>schema</code> call at the top of this file to this block form:</p>

<pre><code class="language-ruby">module Bookshelf
  module Relations
    class Books &lt; Bookshelf::DB::Relation
      schema :books, infer: true do
        associations do
          has_many :reviews
        end
      end
      # ...
</code></pre>

<p>This defines the association, but doesn’t tell us much about how to use it. Fortunately, there’s this guide for that.</p>

<p>If we exit out of our Hanami console and reload back into it, we can now use this association. First we’ll load the <code>books</code> relation:</p>

<pre><code class="language-ruby">books = app["relations.books"]
</code></pre>

<p>Then we can load the first book <em>and</em> all its reviews by using a method called <code>combine</code>:</p>

<pre><code class="language-ruby">books.by_pk(1).combine(:reviews).first
</code></pre>

<p>This will now return a hash of all the data for both the book and its reviews:</p>

<pre><code class="language-ruby">{:id=&gt;1,
 :title=&gt;"Hanami for Rails Developers",
 :author=&gt;"Ryan Bigg",
 :year=&gt;2027,
 :reviews=&gt;[
  {
    :id=&gt;1,
    :book_id=&gt;1,
    :content=&gt;"I now finally understand Hanami!",
    :rating=&gt;5,
    :created_at=&gt;2025-10-13 07:19:48 +1100
  }
  ]
}
</code></pre>

<p>ROM will do this by running first a query to load the book:</p>

<pre><code>SELECT `books`.`id`, `books`.`title`, `books`.`author`, `books`.`year`
FROM `books` WHERE (`books`.`id` = 1) ORDER BY `books`.`id`
</code></pre>

<p>Then another query to load the reviews:</p>

<pre><code>SELECT `reviews`.`id`, `reviews`.`book_id`, `reviews`.`content`, `reviews`.`rating`, `reviews`.`created_at`
FROM `reviews`
INNER JOIN `books` ON (`books`.`id` = `reviews`.`book_id`)
WHERE (`reviews`.`book_id` IN (1))
ORDER BY `reviews`.`id`
</code></pre>

<p>In a Hanami application, we load all the data we need up front, rather than letting method calls way down in the view template dictate what queries are run. This way, there’s no surprises like N+1 queries.</p>

<p>This combination can be setup to happen the other way as well. When we define an association from review to book, over in <code>app/relations/reviews.rb</code>:</p>

<pre><code class="language-ruby">module Bookshelf
  module Relations
    class Reviews &lt; Bookshelf::DB::Relation
      schema :reviews, infer: true do
        associations do
          belongs_to :book
        end
      end
    end
  end
end
</code></pre>

<p>With this association defined, we’ll be able to load a review and its associated book:</p>

<pre><code class="language-ruby">reviews = app["relations.reviews"]
reviews.by_pk(1).combine(:book).first
</code></pre>

<p>This code will return all the information about a review and its book:</p>

<pre><code class="language-ruby">{:id=&gt;1,
 :book_id=&gt;1,
 :content=&gt;"I now finally understand Hanami!",
 :rating=&gt;5,
 :created_at=&gt;2025-10-13 07:19:48 +1100,
 :updated_at=&gt;2025-10-13 07:19:48 +1100,
 :book=&gt;{
   :id=&gt;1,
   :title=&gt;"Hanami for Rails Developers",
   :author=&gt;"Ryan Bigg",
   :year=&gt;2027}
 }
</code></pre>

<p>If we go back to the “book and its reviews” method, we can expose this method to our application through our <code>BookRepo</code> by defining this method in <code>app/repos/book_repo.rb</code>:</p>

<pre><code class="language-ruby">def find_with_reviews(id)
  books.by_pk(id).combine(:reviews).one!
end
</code></pre>

<p>When we go to load a book in our application, we could now use <code>find_with_reviews</code> to load that book and its reviews. We can do this in our <code>show</code> view by changing the code in <code>app/views/books/show.rb</code> to this:</p>

<pre><code class="language-ruby"># frozen_string_literal: true

module Bookshelf
  module Views
    module Books
      class Show &lt; Bookshelf::View
        include Deps["repos.book_repo"]

        expose :book do |id:|
          book_repo.find_with_reviews(id)
        end
      end
    end
  end
end
</code></pre>

<p>In the matching template, it then becomes a cinch to iterate through the reviews. We can do this by updating <code>app/templates/books/show.html.erb</code> to contain this new code:</p>

<pre><code class="language-erb">&lt;h2&gt;Reviews&lt;/h2&gt;

&lt;% reviews.each do |review| %&gt;
  &lt;%= review.class %&gt;
  &lt;p&gt;
    &lt;strong&gt;&lt;%= review.rating %&gt; / 5 &lt;/strong&gt;
    &lt;%= review.content %&gt;
  &lt;/p&gt;
&lt;% end %&gt;
</code></pre>

<h3 id="a-more-complicated-query">A more complicated query</h3>

<p>Defining a <code>has_many</code> or <code>belongs_to</code> association feels like table stakes for a web app these days. Let’s look at something more complicated than this to round out the end of this guide. Let’s say that we want to add a few methods to find:</p>

<ol>
  <li>Books that are well-reviewed (&gt;= 10 reviews)</li>
  <li>Books that have an average review rating above 3</li>
  <li>Books that have an average review rating below 2</li>
</ol>

<p>In a Rails application for the 1st of these queries we would write something like this:</p>

<pre><code class="language-ruby">Book
  .joins(:reviews)
  .group(:id)
  .having('COUNT(reviews.id) &gt;= 10')
</code></pre>

<p>This will generate a query with an <code>INNER JOIN</code> between the <code>books</code> and <code>reviews</code> table, with a <code>GROUP</code> statement on <code>books.id</code>, and a <code>HAVING</code> statement that uses the raw SQL we’ve passed in.</p>

<p>In a Rails app, we would add this code to our model. But in a Hanami application we’ll have to do this on our relation. Let’s define a method in <code>app/relations/books.rb</code> for this now:</p>

<pre><code class="language-ruby">def popular
  join(:reviews)
    .group(:id)
    .having { count(reviews[:id]) &gt;= 10 }
end
</code></pre>

<p>The syntax provided by Sequel isn’t too much different, until we get to the final line. There we evaluate a block passed into <code>having</code>, and we’re able to use the <code>reviews</code> relation from within our books relation. Instead of writing raw SQL, the underlying Sequel gem provides us a clean Ruby syntax to use instead.</p>

<p>We <em>could</em> still write the <code>having</code> statement with raw SQL, but we’d have to call that out explicitly with <code>Sequel.lit</code>:</p>

<pre><code class="language-ruby">join(:reviews)
  .group(:id)
  .having(Sequel.lit("count(reviews.id) &gt; 10"))
</code></pre>

<p>This syntax is slightly longer than the Ruby version, and a bit more punctuation-heavy too. It’s for this reason that I try to opt for the Ruby syntax when I can find a Sequel version of that.</p>

<p>If we run <code>hanami console</code>, we can then use this new method:</p>

<pre><code class="language-ruby">books = app["relations.books"]
books.popular
</code></pre>

<p>This will show the query it could run:</p>

<pre><code class="language-sql">SELECT `books`.`id`, `books`.`title`, `books`.`author`, `books`.`year`
FROM `books`
INNER JOIN `reviews` ON (`books`.`id` = `reviews`.`book_id`)
GROUP BY `books`.`id`
HAVING (count(`reviews`.`id`) &gt;= 10)
ORDER BY `books`.`id`
</code></pre>

<p>This looks great! We don’t have enough reviews for this method at the moment. We can create a few:</p>

<pre><code>10.times { reviews.insert(rating: 5, content: "Great!", book_id: 1) }
</code></pre>

<p>And now if we ask for the popular book, we’ll see it’s returned:</p>

<pre><code class="language-ruby">books.popular.first
</code></pre>

<p>This gives us:</p>

<pre><code>=&gt; {:id=&gt;1, :title=&gt;"Hanami for Rails Developers", :author=&gt;"Ryan Bigg", :year=&gt;2027}
</code></pre>

<p>We’ve got the first method added, now let’s look at finding books where the review average rating is above a 3:</p>

<pre><code class="language-ruby">def liked
  join(:reviews)
  .group(:id)
  .having { avg(reviews.rating) &gt; 3 }
end
</code></pre>

<p>This time we use an <code>avg</code> method to generate an <code>AVG</code> aggregation query for our reviews. Let’s exit the <code>hanami console</code> and restart it again to pick up this new method. Now we’ll try to use it:</p>

<pre><code class="language-ruby">books = app["relations.books"]
books.liked
</code></pre>

<p>This will show us this query:</p>

<pre><code class="language-sql">SELECT `books`.`id`, `books`.`title`, `books`.`author`, `books`.`year`
FROM `books`
INNER JOIN `reviews` ON (`books`.`id` = `reviews`.`book_id`)
GROUP BY `books`.`id`
HAVING (avg(`reviews`.`rating`) &gt;= 3)
ORDER BY `books`.`id`
</code></pre>

<p>That looks great! How about we get both <code>popular</code> and <code>liked</code> books?</p>

<pre><code>books.popular.liked
</code></pre>

<p>This time the query is:</p>

<pre><code class="language-sql">SELECT `books`.`id`, `books`.`title`, `books`.`author`, `books`.`year`
FROM `books`
INNER JOIN `reviews` ON (`books`.`id` = `reviews`.`book_id`)
INNER JOIN `reviews` ON (`books`.`id` = `reviews`.`book_id`)
HAVING ((count(`reviews`.`id`) &gt;= 10) AND (avg(`reviews`.`rating`) &gt;= 3))
ORDER BY `books`.`id`
</code></pre>

<p>No, you’re not having vision issues, there are indeed <em>two</em> joins to reviews! This is because both of our methods tell the relation to join the reviews table. If we attempt to run this query, SQL will be unable to disambiguate between which <code>reviews</code> table we mean.</p>

<p>What do we do in these situations, then? Well, we add a <em>third</em> method that does the join first:</p>

<pre><code class="language-ruby">def with_reviews
  join(:reviews)
    .group(:id)
end

def popular
  join(:reviews).having { count(reviews[:id]) &gt;= 10 }
end

def liked
  join(:reviews).having { avg(reviews[:rating]) &gt;= 3 }
end
</code></pre>

<p>Now this will mean we’ll be able to call <code>books.with_reviews.popular</code> to get the popular books, and <code>books.with_reviews.liked</code> to get the liked books, and then <code>books.with_reviews.popular.liked</code> to get the popular liked books!</p>

<p>Before we move on from here, we can add our other method to find the books with low-scoring reviews:</p>

<pre><code class="language-ruby">def disliked
  join(:reviews).having { avg(reviews[:rating]) &gt;= 2 }
end
</code></pre>

<p>This syntax with <code>with_reviews</code> is going to be a mouthful. Fortunately, we can provide a clean interface by exposing these methods through our <code>BookRepo</code> class back to our application. Let’s add in a few methods in <code>app/repos/book_repo.rb</code></p>

<pre><code class="language-ruby">def with_reviews
  books.with_reviews
end

def popular
  with_reviews.popular
end

def popular_and_liked
  with_reviews.popular.liked
end

def popular_and_disliked
  with_reviews.popular.disliked
end
</code></pre>

<p>Our repository is now going to provide a cleaner facade back to our application, so that we can make calls such as <code>book_repo.popular</code> to get back a list of popular books, and the repo will take care of the <code>with_reviews</code> joining.</p>

<p>We can see here with the code in the relation and repository that the relation is taking care of the messy SQL-adjacent code, while the repository is using the methods of the relation to then provide a cleaner interface back up to the application.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Part 1: Models Part 2: Controllers Part 3: Forms Part 4: Associations (you are here)]]></summary></entry><entry><title type="html">Hanami for Rails Developers: Part 3: Forms</title><link href="https://ryanbigg.com/2025/10/hanami-for-rails-developers-3-forms" rel="alternate" type="text/html" title="Hanami for Rails Developers: Part 3: Forms" /><published>2025-10-06T00:00:00+00:00</published><updated>2025-10-06T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/10/hanami-for-rails-developers-3-forms</id><content type="html" xml:base="https://ryanbigg.com/2025/10/hanami-for-rails-developers-3-forms"><![CDATA[<p>This blog post is part of a series called “Hanami for Rails Developers”.</p>

<ul>
  <li>Part 1: <a href="/2025/10/hanami-for-rails-developers-1-models">Models</a></li>
  <li>Part 2: <a href="/2025/10/hanami-for-rails-developers-2-controllers">Controllers</a></li>
  <li>Part 3: <a href="/2025/10/hanami-for-rails-developers-3-forms">Forms</a> (you are here)</li>
</ul>

<p>In the first two parts of this guide, we covered off the familiar concepts of models and controllers, and saw how Hanami approached these designs. We saw that Hanami split the responsibilities of models between <strong>repositories</strong>, <strong>relations</strong> and <strong>structs</strong>, and we saw that the responsibilities of a controller and its views were split between <strong>actions</strong>, <strong>views</strong> and <strong>templates</strong>.</p>

<p>In this part, we’re going to continue building on our application’s foundation by introducing a form that lets us add further books to our application. In a Rails app, we would handle this by adding a <code>new</code> and <code>create</code> action to our controller. You’ll see that Hanami isn’t much different here when it comes to that.</p>

<p>We’ll be building out the <code>new</code> and <code>create</code> actions for books in this section, seeing how we can create books by using our existing <code>BookRepo</code> class. We’ll also see how to add validations to our data in this chapter, not on the repository itself, but in the action.</p>

<p>Let’s get stuck in.</p>

<h3 id="the-new-book-form">The New Book Form</h3>

<p>The first thing that we’ll create for this new book form is an action, which we can do with:</p>

<pre><code>hanami g action books.new
</code></pre>

<p>We’ll change the route generated from this action to have a name that we can use later on. Let’s change <code>config/routes.rb</code>:</p>

<pre><code class="language-ruby">get "/books/new", to: "books.new", as: :new_book
</code></pre>

<p>We can then route to this page by updating our template at <code>app/templates/books/index.html.erb</code>. We’ll add a link to this page just under the header on that page:</p>

<pre><code class="language-erb">&lt;h1&gt;Books&lt;/h1&gt;

&lt;%= link_to "New Book", routes.path(:new_book) %&gt;
</code></pre>

<p>This link will take us over to the new book view, which we’ll now need to fill out. The template for that view exists at <code>app/templates/books/new.html.erb</code>:</p>

<pre><code class="language-erb">&lt;h1&gt;New Book&lt;/h1&gt;

&lt;%= form_for :book, routes.path(:create_book) do |f| %&gt;
  &lt;div&gt;
    &lt;%= f.label :title %&gt;
    &lt;%= f.text_field :title %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.label :author %&gt;
    &lt;%= f.text_field :author %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.label :year %&gt;
    &lt;%= f.number_field :year %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.submit "Create Book" %&gt;
  &lt;/div&gt;
&lt;% end %&gt;
</code></pre>

<p>This <code>form_for</code> helper looks a lot like Rails’ own, but varies in that it takes positional arguments, rather than keyword arguments. The first argument dictates the naming of the parameters that this form will submit. This means everything will be sent to action under <code>params[:book]</code>. The second parameter is the route to create a book, which does not yet exist.</p>

<p>Let’s create that action and subsequent route now:</p>

<pre><code>hanami g action books.create
</code></pre>

<p>We’ll change the route to have a name by updating the line in <code>config/routes.rb</code> to this:</p>

<pre><code>post "/books", to: "books.create", as: :create_book
</code></pre>

<p>After adding this route, our form will now be able to render and display:</p>

<p><img src="/images/hanami/new_book.jpg" alt="New book" /></p>

<p>Next up, we need to give this form somewhere to submit to. To work with what this form submits, we’ll update the <code>books.create</code> action code in <code>app/actions/books/create.rb</code>:</p>

<pre><code class="language-ruby"># frozen_string_literal: true

module Bookshelf
  module Actions
    module Books
      class Create &lt; Bookshelf::Action
        include Deps["repos.book_repo"]

        def handle(request, response)
          book = book_repo.create(request.params[:book])
          response.flash[:success] = "Book created successfully"

          response.redirect_to routes.path(:book, id: book.id)
        end
      end
    end
  end
end
</code></pre>

<p>You’ll notice that this action is a lot like a regular <code>create</code> action within Rails, with a few clear differences. In the Hanami action, we’re pulling <code>params</code> from <code>request</code>, as we did in the last part with the <code>year</code> parameter. We’re also working with the <code>response</code> object here, setting the flash and <code>redirect_to</code> specifically on those objects.</p>

<p>To use <code>flash</code> within a Hanami application, we need to add session support to the application. Hanami applications don’t come with this enabled by default, because they may instead be used in an API-only context. To add this session support, we’ll go to Hanami’s application configuration file, <code>config/app.rb</code>, and add this line:</p>

<pre><code class="language-ruby">require "hanami"

module Bookshelf
  class App &lt; Hanami::App
    config.sessions = :cookie, { secret: "your_secret_key_goes_here" }
  end
end
</code></pre>

<p>With the session support added, our flash message will be stored correctly. But we’re currently not <em>displaying</em> that flash message anywhere! In a Rails application you would put this kind of thing in <code>app/views/layouts/application.html.erb</code>. Hanami has a different path, which is <code>app/templates/layouts/app.html.erb</code>. Let’s add the flash there just under the <code>&lt;body&gt;</code> tag:</p>

<pre><code class="language-erb">&lt;% if flash[:success] %&gt;
  &lt;div class="flash flash-success"&gt;&lt;%= flash[:success] %&gt;&lt;/div&gt;
&lt;% end %&gt;
</code></pre>

<p>Now that we’ve setup the rendering of our flash message, there’s one final piece we need to do. Our <code>BookRepo</code> doesn’t know how to create a book. We can add this feature to <code>BookRepo</code> by adding this line:</p>

<pre><code class="language-ruby">module Bookshelf
  module Repos
    class BookRepo &lt; Bookshelf::DB::Repo
      commands :create
</code></pre>

<p>The <code>commands</code> method comes from the ROM series of gems, that Hanami uses under-the-hood as its persistence layer. ROM provides some simple commands that reproduce common behaviour, and <code>create</code> is one of these.</p>

<p>That’ll be all we need to create a new book now. When we try out the form now, we’ll see that a book can be created:</p>

<p><img src="/images/hanami/created_book.jpg" alt="Created book" /></p>

<h3 id="adding-validations">Adding validations</h3>

<p>Now that we’ve got the happy path working for creating a book, let’s work on adding some validations to this form so that books can no longer be submitted without an author or title.</p>

<p>To add validations in an Hanami application, we add them to the action that processes the parameters, which would be the <code>Books::Create</code> action in our app. Let’s add this validation to <code>app/actions/books/create.rb</code> now:</p>

<pre><code class="language-ruby">module Bookshelf
  module Actions
    module Books
      class Create &lt; Bookshelf::Action
        include Deps["repos.book_repo"]

        params do
          required(:book).schema do
            required(:title).filled(:string)
            required(:author).filled(:string)
            optional(:year).maybe(:integer)
          end
        end

        # ...
</code></pre>

<p>This syntax uses another gem from the same organisation as Hanami called <a href="https://dry-rb.org/gems/dry-schema/1.5/"><code>dry-schema</code></a>. It validates our parameters when we take them in, rather than throwing yet another responsibility into the model class.</p>

<p>This syntax validates that <code>title</code> and <code>author</code> are both filled in, and must be a string. It also validates <code>year</code>, but only that if it’s provided it’s going to be an integer, rather than any other type.</p>

<p>On top of this, our parameters are now restricted to accepting only those specified in this set. This syntax both provides the same style of validation that <code>validates presence: true</code> would provide in a Rails model, and <em>also</em> the same features that <code>strong_parameters</code> (<code>params.require(:book).permit(:title, ...)</code>) would in a Rails application. Our validation logic now sits in one place, the action, rather than across two different places.</p>

<p>Next up, we’ll need to have the behaviour of this <code>create </code>action do different things depending on if the parameters are valid or not. Let’s update this action to do that now. We’ll change the <code>handle</code> method of this action to this:</p>

<pre><code class="language-ruby">def handle(request, response)
  unless request.params.valid?
    response.flash.now[:error] = "Your book could not be created"
    response.render(new_view,
      errors: request.params.errors[:book].to_h
    )

    return
  end

  book = book_repo.create(request.params[:book])
  response.flash[:success] = "Book created successfully"

  response.redirect_to routes.path(:book, id: book.id)
end
</code></pre>

<p>This action now checks to see if the parameters passed in are valid or not. If they’re not, we’ll display a flash message and render the new view, passing it the errors from the validation. If the parameters <em>are</em> valid, then we go ahead with the action as before.</p>

<p>Our new code refers to something called <code>new_view</code>, which we don’t have yet. To get that, we need to bring that in as a dependency at the top of this class:</p>

<pre><code class="language-ruby">include Deps["repos.book_repo"]
include Deps[new_view: "views.books.new"]
</code></pre>

<p>When we import dependencies in Hanami, it will use the last part of the name as the name for the method that becomes available to refer to that dependency. We can pick a different name here, by using Hash syntax where the key is the name we want, and the value is the dependency. If we didn’t give this dependency a different name in this case, we would have to refer to it as <code>new</code>, which is confusing to see by itself.</p>

<p>When the form fails validation, we’ll re-render the <code>new</code> action passing it errors. If we want to display those errors in the template, we’ll need to expose them from the action. Let’s go to <code>app/actions/books/new.rb</code> and add an <code>expose</code> for that:</p>

<pre><code class="language-ruby"># frozen_string_literal: true

module Bookshelf
  module Views
    module Books
      class New &lt; Bookshelf::View
        expose :errors
      end
    end
  end
end
</code></pre>

<p>To display these errors at the top of the form, we’ll put this code into <code>app/templates/books/new.html.erb</code>:</p>

<pre><code class="language-erb">&lt;h1&gt;New Book&lt;/h1&gt;

&lt;% if errors %&gt;
  &lt;div id="error_explanation"&gt;
    &lt;h2&gt;Your book could not be created:&lt;/h2&gt;
    &lt;% errors.each do |field, field_errors| %&gt;
      &lt;p&gt;&lt;%= inflector.humanize(field) %&gt; &lt;%= field_errors.join(", ") %&gt;&lt;/p&gt;
    &lt;% end %&gt;
  &lt;/div&gt;
&lt;% end %&gt;
</code></pre>

<p>We can use <code>errors</code> here as we’ve exposed them from the view. We then iterate through them, using Hanami’s built in <code>inflector</code> to turn these field names into something human-readable. They would be <code>title</code> and <code>author</code>, but they’re now <code>Title</code> and <code>Author</code>. It’s not much, but it’ll do the job.</p>

<p>If we attempt to fill out the book form now, but leave either title or author blank, we’ll see errors:</p>

<p><img src="/images/hanami/invalid_book.jpg" alt="Invalid book" /></p>

<p>And if we fill out those fields, we’ll see that we’ve successfully created a book.</p>

<h3 id="edit-form">Edit Form</h3>

<p>Now that we’re able to create a book, we’re going to want to continue on completing the set of all the RESTful actions, including editing and updating. So let’s see what it’s going to take to do this in Hanami. Just like we did for the <code>new</code> and <code>create</code> actions, we’re going to need to generate the pair of actions for <code>edit</code> and <code>update</code>. Let’s run the generator now for both of them:</p>

<pre><code>hanami g action books.edit
hanami g action books.update
</code></pre>

<p>After generating these actions, we’ll give their routes names so that we can refer to them later. Let’s go into <code>config/routes.rb</code> and update the last two lines to this:</p>

<pre><code class="language-ruby">get "/books/:id/edit", to: "books.edit", as: :edit_book
patch "/books/:id", to: "books.update", as: :update_book
</code></pre>

<p>To be able to navigate to the edit page, we’ll add a small link in our <code>show</code> template using this <code>edit_book</code> path, at <code>app/templates/books/show.html.erb</code>:</p>

<pre><code class="language-erb">&lt;h1&gt;&lt;%= book.title %&gt;&lt;/h1&gt;

&lt;%= link_to "Edit", routes.path(:edit_book, id: book.id) %&gt;
</code></pre>

<p>Now it’s time for the edit view itself. We have a perfectly good form over in <code>app/templates/books/new.html.erb</code>, and the way we would share this form in a Rails application between a <code>new</code> and <code>edit</code> view is to turn it into a partial. Hanami has the same style of support too! So we can move all of this code out of the <code>new</code> template, and into a new template at <code>app/templates/books/_form.html.erb</code>:</p>

<pre><code class="language-erb">&lt;% if errors %&gt;
  &lt;div id="error_explanation"&gt;
    &lt;h2&gt;Your book could not be created:&lt;/h2&gt;
    &lt;% errors.each do |field, field_errors| %&gt;
      &lt;p&gt;&lt;%= inflector.humanize(field) %&gt; &lt;%= field_errors.join(", ") %&gt;&lt;/p&gt;
    &lt;% end %&gt;
  &lt;/div&gt;
&lt;% end %&gt;

&lt;%= form_for :book, routes.path(:create_book) do |f| %&gt;
  &lt;div&gt;
    &lt;%= f.label :title %&gt;
    &lt;%= f.text_field :title %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.label :author %&gt;
    &lt;%= f.text_field :author %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.label :year %&gt;
    &lt;%= f.number_field :year %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.submit "Create Book" %&gt;
  &lt;/div&gt;
&lt;% end %&gt;

</code></pre>

<p>Then in our <code>app/templates/books/new.html.erb</code> file, we can render this same content with:</p>

<pre><code>&lt;%= render "form", errors: errors %&gt;
</code></pre>

<p>The <code>render</code> method here takes in the name of the partial and then any local variable we would like to make available to that partial.</p>

<p>We’ll now update our <code>app/templates/books/edit.html.erb</code> to use this same template:</p>

<pre><code class="language-erb">&lt;h1&gt;Editing a book&lt;/h1&gt;

&lt;%= render "form", errors: nil %&gt;
</code></pre>

<p>We’re leaving out <code>errors</code> here for the moment, as we haven’t gotten to implementing that part just yet.</p>

<p>When we’re rendering this form, we would like the fields to be automatically populated with what’s in the database. To do this, we need to load the book from the database and to load the book we’ll need the parameter to be passed in from the action. Let’s set that up now in <code>app/actions/books/edit.rb</code>:</p>

<pre><code class="language-ruby">module Bookshelf
  module Actions
    module Books
      class Edit &lt; Bookshelf::Action
        def handle(request, response)
          response.render(view, id: request.params[:id])
        end
      end
    end
  end
end
</code></pre>

<p>With the parameter passed in, we can now proceed with loading the book over in <code>app/views/books/edit.rb</code>:</p>

<pre><code class="language-ruby">module Bookshelf
  module Views
    module Books
      class Edit &lt; Bookshelf::View
        include Deps["repos.book_repo"]

        expose :book do |id:|
          book_repo.find(id)
        end
      end
    end
  end
end
</code></pre>

<p>We load the book by bringing in the <code>book_repo</code> dependency, and using the <code>find</code> method on that to load the book, pulling the <code>id</code> parameter out of the block argument for <code>expose</code>. Because this <code>expose</code> shares a name with the first argument to <code>form_for</code>, it will populate the form automatically. If we go to http://localhost:2300/books/1/edit, we’ll see the form is populated:</p>

<p><img src="/images/books/editing_book.jpg" alt="Editing a book" /></p>

<p>There’s an issue with the form at the moment that if we submit it, it’s going to create a duplicate of the book that we’ve got there rather than updating the existing book. This is because in the <code>app/templates/books/_form.html.erb</code> partial, we’re telling the form the route is this:</p>

<pre><code class="language-erb">&lt;%= form_for :book, routes.path(:create_book) do |f| %&gt;
</code></pre>

<p>The form partial needs to understand that we want to go to different actions, depending on how it’s being rendered. Rails has some smarts in it to determine the route based on if the record is either new or persisted. Hanami does not have these smarts in it (yet). So we have to be the smart ones instead.</p>

<p>We’ll change how we render this form partial in <code>app/templates/books/edit.html.erb</code> to this:</p>

<pre><code class="language-erb">&lt;%= render "form",
  book: book,
  path: routes.path(:book, id: book.id),
  form_type: :update
%&gt;
</code></pre>

<p>This passes in two other local variables that we’ll use to determine where to take the form. While we’re making this change for edit, we’ll also make the change for the <code>new</code> template too:</p>

<pre><code class="language-erb">&lt;%= render "form",
  book: book,
  errors: errors,
  path: routes.path(:create_book)
  form_type: :create
%&gt;
</code></pre>

<p>Now that we’re passing these through to the partial, we’ll update the partial to handle both <code>path</code> and <code>form_type</code> by changing <code>app/templates/books/_form.html.erb</code> to this:</p>

<pre><code class="language-erb">&lt;% if errors %&gt;
  &lt;div id="error_explanation"&gt;
    &lt;h2&gt;Your book could not be &lt;%= form_type == :create ? "created" : "updated" %&gt;:&lt;/h2&gt;
    &lt;% errors.each do |field, field_errors| %&gt;
      &lt;p&gt;&lt;%= inflector.humanize(field) %&gt; &lt;%= field_errors.join(", ") %&gt;&lt;/p&gt;
    &lt;% end %&gt;
  &lt;/div&gt;
&lt;% end %&gt;

&lt;%= form_for :book, path, method: form_type == :create ? :post : :patch do |f| %&gt;
  &lt;div&gt;
    &lt;%= f.label :title %&gt;
    &lt;%= f.text_field :title %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.label :author %&gt;
    &lt;%= f.text_field :author %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.label :year %&gt;
    &lt;%= f.number_field :year %&gt;
  &lt;/div&gt;
  &lt;div&gt;
    &lt;%= f.submit form_type == :create ? "Create Book" : "Update Book" %&gt;
  &lt;/div&gt;
&lt;% end %&gt;
</code></pre>

<p>The three changes here are:</p>

<ol>
  <li>Changing the errors box to say “Your book could not be created/updated”</li>
  <li>Changing the path and the method of the form based on <code>form_type</code></li>
  <li>Changing the wording of the submit button based on <code>form_type</code>.</li>
</ol>

<p>This will set up the form partial when rendered by the <code>edit</code> view to submit to the <code>update</code> action, while still maintaining its ability to submit to the <code>create</code> view when rendered by the <code>new</code> view.</p>

<p>Speaking of <code>update</code> actions, let’s write one now in <code>app/actions/books/update.rb</code>. We’ll start by including the book repo as a dependency and defining the parameters that our request will work with:</p>

<pre><code class="language-ruby">module Bookshelf
  module Actions
    module Books
      class Update &lt; Bookshelf::Action
        include Deps["repos.book_repo"]

        params do
          required(:id).filled(:integer)
          required(:book).schema do
            required(:title).filled(:string)
            required(:author).filled(:string)
            optional(:year).maybe(:integer)
          end
        end
      end
    end
  end
end
</code></pre>

<p>These parameters are the same as from the <code>create</code> action with one exception: we now need to <em>also</em> take in the <code>id</code> parameter. If we were to leave that out of the <code>params</code> specification here, we couldn’t access it within our action as it wouldn’t have been in the permitted set of parameters for this action.</p>

<p>With the parameters defined, we can now write the <code>handle</code> method:</p>

<pre><code class="language-ruby">def handle(request, response)
  unless request.params.valid?
    response.flash.now[:error] = "This book could not be updated"
    response.render(edit_view,
      id: request.params[:id],
      errors: request.params.errors[:book].to_h,
    )

    return
  end

  book_repo.update(request.params[:id], request.params[:book])
  response.flash[:success] = "Book updated successfully"

  response.redirect_to routes.path(:book, id: request.params[:id])
end
</code></pre>

<p>This action works similarly to <code>create</code>, except we’re going to be updating a book rather than creating it. We’re referring to <code>edit_view</code> here, but we haven’t yet defined that. Let’s import that as well at the top of this action:</p>

<pre><code class="language-ruby">include Deps[edit_view: "views.books.edit"]
</code></pre>

<p>To make the <code>book_repo</code> accept a call to <code>update</code>, we’ll need to add a command to <code>app/repos/book_repo.rb</code>:</p>

<pre><code class="language-ruby">module Bookshelf
  module Repos
    class BookRepo &lt; Bookshelf::DB::Repo
      commands :create, update: :by_pk
</code></pre>

<p>This command takes a second argument to determine which method from the <code>books</code> relation to use when looking up a book to update.</p>

<p>That’ll handle the successful flow of updating our book, but we also need to pay attention to the unsuccessful flow as well. The <code>edit</code> view will receive <code>errors</code>, which it will need to expose. Let’s update <code>app/actions/books/edit.rb</code> to this:</p>

<pre><code class="language-ruby">module Bookshelf
  module Views
    module Books
      class Edit &lt; Bookshelf::View
        include Deps["repos.book_repo"]
        expose :errors

        expose :book do |id:|
          book_repo.find(id)
        end
      end
    end
  end
end
</code></pre>

<p>This will take in the errors from the re-rendering of this view from a failed <code>update</code>, and render a form with the errors.</p>

<p>If we attempt to update a book correctly now, we’ll see it works:</p>

<p><img src="/images/hanami/updated_book.jpg" alt="Updated book" /></p>

<p>And if we attempt to update it with invalid data, it will fail:</p>

<p><img src="/images/hanami/book_update_error.jpg" alt="Updated book errors" /></p>]]></content><author><name></name></author><summary type="html"><![CDATA[This blog post is part of a series called “Hanami for Rails Developers”.]]></summary></entry><entry><title type="html">Hanami for Rails Developers: Part 1: Models</title><link href="https://ryanbigg.com/2025/10/hanami-for-rails-developers-1-models" rel="alternate" type="text/html" title="Hanami for Rails Developers: Part 1: Models" /><published>2025-10-05T00:00:00+00:00</published><updated>2025-10-05T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/10/hanami-for-rails-developers-1-models</id><content type="html" xml:base="https://ryanbigg.com/2025/10/hanami-for-rails-developers-1-models"><![CDATA[<p>This blog post is part of a series called “Hanami for Rails Developers”.</p>

<ul>
  <li>Part 1: <a href="/2025/10/hanami-for-rails-developers-1-models">Models</a> (you are here)</li>
  <li>Part 2: <a href="/2025/10/hanami-for-rails-developers-2-controllers">Controllers</a></li>
  <li>Part 3: <a href="/2025/10/hanami-for-rails-developers-3-forms">Forms</a></li>
  <li>Part 4: <a href="/2025/10/hanami-for-rails-developers-4-associations">Associations</a></li>
</ul>

<p>There’s plenty of writing out there for <em>why</em> you should use Hanami, and so this post won’t cover that. If you want those thoughts, see my <a href="https://ryanbigg.com/2022/11/hanami-20-thoughts">Hanami 2.0 thoughts</a> and my earlier <a href="https://ryanbigg.com/2018/03/my-thoughts-on-hanami">thoughts on Hanami</a> posts.</p>

<p>This post covers off how to get started with Hanami, with a focus on those who are familiar with Rails and the MVC structure it provides. I’m unashamedly going to crib parts of this from the <a href="https://guides.hanamirb.org/v2.3/introduction/getting-started/">Hanami Getting Started Guide</a>, but explain them in a different way.</p>

<p>With a Rails app, you’ll be familiar with the Model-View-Controller pattern. Hanami has adopted this pattern too, but has a take on it where the concerns are split across more distinct types of classes. This leads to a better separation of concerns and an easier-to-maintain application.</p>

<p>Hanami’s layers of separation are designed with the intent of making long-term maintenance of your application easier. The layers that Hanami introduce don’t come from nowhere. They come out of decades of professionally building Rails applications and realizing what would make maintenance of those applications easier.</p>

<p>In Part 1 of this series, I’m going to cover off how Hanami applications interact with databases.</p>

<h2 id="the-model-layer">The Model Layer</h2>

<p>Whenever you’re building a Rails application you typically want to pull data from a data source. When you’re building a Hanami application, you’ll want to do the same thing. Rather than having one model class to use as a dumping ground, Hanami separates these into a few distinct classes called repositories, relations and structs.</p>

<ol>
  <li><strong>Repositories</strong>: Defines the interactions between your database and your application.</li>
  <li><strong>Relations</strong>: Provides a home for your application’s complicated queries.</li>
  <li><strong>Structs</strong>: Represents rows from your database in plain and simple Ruby objects.</li>
</ol>

<p>Let’s take a look at each of these in turn by creating a table called <code>books</code>, and then inserting data into that table, and then requesting that data back out in various ways.</p>

<h3 id="migrations">Migrations</h3>

<p>Hanami, like Rails, supports database migrations. To create a migration, we use this command:</p>

<pre><code>hanami g migration create_books
</code></pre>

<p>This migration syntax uses ROM – Hanami’s choice for a database library – and is currently empty. The migrations in Hanami live in <code>config/db/migrate</code>, rather than the <code>db/migrate</code> of Rails. The reason for this is that migrations are <em>configuration for your database</em>.</p>

<p>Let’s see that migration file now in <code>config/db/migrate</code>:</p>

<pre><code class="language-ruby">ROM::SQL.migration do
  # Add your migration here.
  #
  # See https://guides.hanamirb.org/v2.2/database/migrations/ for details.
  change do
  end
end
</code></pre>

<p>We can fill out this migration to create the <code>books</code> table this way.</p>

<pre><code class="language-ruby">ROM::SQL.migration do
  change do
    create_table :books do
      primary_key :id
      column :title, :text, null: false
      column :author, :text, null: false
    end
  end
end
</code></pre>

<p>The syntax used here is not too dissimilar to what you’d see in a Rails migration. Notably, we have to include the <code>primary_key</code> here, whereas in Rails it comes automatically pre-defined. The migration feature comes from a gem called <code>rom-sql</code>, which itself uses another gem called <code>sequel</code>. The migration syntax itself comes from <code>sequel</code>. You can <a href="https://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html">read more about Sequel migrations here</a></p>

<p>We can run this migration with:</p>

<pre><code>hanami db migrate
</code></pre>

<p>With our table now existing in our database, we need something to insert and read data from that table. That “something” is called a relation.</p>

<h3 id="relations">Relations</h3>

<p>We can generate a relation using this command:</p>

<pre><code>hanami g relation books
</code></pre>

<p>Relations in Hanami are pluralised, and match the name of the table. We can use this relation to insert some data by booting up the console:</p>

<pre><code>hanami console
</code></pre>

<p>Hanami provides a <em>registry</em> for our applications classes, and we can use this registry to get the relation:</p>

<pre><code class="language-ruby">books = app["relations.books"]
</code></pre>

<p>We’ll see this relation is already configured with our database, thanks to some setup taken care of by Hanami. Rails would do the same thing, but calls it <code>connection</code> on Active Record models.</p>

<pre><code class="language-ruby">#&lt;Bookshelf::Relations::Books name=ROM::Relation::Name(books) dataset=#&lt;Sequel::SQLite::Dataset...
</code></pre>

<p>We can insert a book into our table by running:</p>

<pre><code class="language-ruby">books.insert(title: "Hanami for Rails Developers", author: "Ryan Bigg")
</code></pre>

<p>This will simply return <code>1</code> as its the ID of the record that was inserted into the database. This may be surprising to Rails developers, who are used to getting instances back straight away from an <code>insert</code> request. To get back to the data that’s in the database, we can run:</p>

<pre><code>book = books.first
</code></pre>

<p>We will now see the data as a Hash:</p>

<pre><code>=&gt; {:id=&gt;1, :title=&gt;"Hanami for Rails Developers", :author=&gt;"Ryan Bigg"}
</code></pre>

<p>The relation for Hanami works with data in its barest form. We passed a Hash to <code>insert</code>, and got one back for <code>first</code>. To get back proper Ruby objects, we need a repository.</p>

<h3 id="repository">Repository</h3>

<p>Let’s generate a repository for our <code>books</code> table now, by exiting our <code>hanami console</code> session (with <code>exit</code>) then running this:</p>

<pre><code>hanami g repo book
</code></pre>

<p>Repositories in Hanami are singularized, but relations are pluralized. This is because relations are working on your table, which is a collection of data. Repositories on the other hand represent a single type of that data, in this case <code>Book</code>. So the repository representing that type is called <code>BookRepo</code>.</p>

<p>We can use this repository in the console by jumping back in with <code>hanami console</code> and then running:</p>

<pre><code class="language-ruby">book_repo = app["repos.book_repo"]
</code></pre>

<p>To fetch the book we inserted, we can run:</p>

<pre><code class="language-ruby">book_repo.books.first
</code></pre>

<p>This method calls <code>books</code>, which access the matching relation from the repository. Then it calls <code>first</code> on that relation.</p>

<p>An interesting thing happens here: this will return a structured version of our data.</p>

<pre><code>=&gt; #&lt;Bookshelf::Structs::Book id=1 title="Hanami for Rails Developers" author="Ryan Bigg"&gt;
</code></pre>

<p>We get this ability by using the relation through the repository.</p>

<p>The returned object here has very few methods on it. Just enough methods to represent the data from the row, and that’s it.</p>

<p>Calling <code>book_repo.books.&lt;whatever method&gt;</code> is going to get old very quickly, and that leads us to the point of repositories. We can provide shorter methods by adding them to our repository. Let’s add a <code>find</code> and an <code>all</code> method to our repository, over in <code>app/repos/book_repo.rb</code>:</p>

<pre><code class="language-ruby">module Bookshelf
  module Repos
    class BookRepo &lt; Bookshelf::DB::Repo
      def find(id)
        books.by_pk(id).one
      end

      def all
        books.to_a
      end
    end
  end
end
</code></pre>

<p>This method can then be used to find our book based on the table’s primary key. Let’s exit the console, start it again and try that now:</p>

<pre><code class="language-ruby">book_repo = app["repos.book_repo"]
book = book_repo.find(1)
</code></pre>

<p>We’ll get back our book, all without having to type <code>where</code> + <code>first</code>.</p>

<pre><code>=&gt; #&lt;Bookshelf::Structs::Book id=1 title="Hanami for Rails Developers" author="Ryan Bigg"&gt;
</code></pre>

<p>We can also retrieve all of our books by using <code>all</code>:</p>

<pre><code>books = book_repo.all
=&gt; [#&lt;Bookshelf::Structs::Book id=1 title="Hanami for Rails Developers" author="Ryan Bigg"&gt;]
</code></pre>

<h3 id="scoping-queries">Scoping queries</h3>

<p>To further demonstrate what a repository and relation do within a Hanami application, we’re now going to perform an action that would be common to a lot of Rails applications: adding a <code>by_year</code> scope to our queries. In Rails, we would add this to a model with this code:</p>

<pre><code class="language-ruby">scope :by_year, -&gt;(year) { where(year: year) }
</code></pre>

<p>This defines a method on the model within Rails. The approach in Hanami is very similar, but instead of defining the method on the model, we define it on the repository. Before we can perform queries against a year column, let’s add it with one more migration. We’ll create this migration with:</p>

<pre><code>hanami g migration add_year_to_books
</code></pre>

<p>We’ll open up that new migration file in <code>config/db/migrate</code> and fill it out this way:</p>

<pre><code class="language-ruby">ROM::SQL.migration do
  change do
    add_column :books, :year, :integer
  end
end
</code></pre>

<p>Let’s run this migration with:</p>

<pre><code>hanami db migrate
</code></pre>

<p>Now that we have a <code>year</code> column, let’s open up <code>app/repos/book_repo.rb</code> and define a method to find books matching a particular year:</p>

<pre><code class="language-ruby">def by_year(year)
  books.where(year: year)
end
</code></pre>

<p>This code can allow us to call <code>book_repo.by_year(2025)</code> to get all the books from the year 2025.</p>

<p>As you can see by these <code>find</code> and <code>by_year</code> methods, we define the methods to interact with our database as we need them within a Hanami application.</p>

<p>Let’s add one more of these to find by the author as well:</p>

<pre><code class="language-ruby">def by_author(author)
  books.where(author: author)
end
</code></pre>

<p>If we do <code>book_repo.by_author("Ryan Bigg")</code> in our console, we’ll get back the book we added earlier on.</p>

<p>Now what about if we wanted to chain these <code>by_author</code> and <code>by_year</code> methods together by calling:</p>

<pre><code class="language-ruby">book_repo.by_year(2025).by_author("Ryan Bigg")
</code></pre>

<p>Well, if we try that out now, we’ll get an error:</p>

<pre><code class="language-ruby">(irb):2:in `&lt;main&gt;': undefined method `by_author' for #&lt;Bookshelf::Relations::Books
</code></pre>

<p>This is because the object returned by <code>by_year</code> is an instance of the relation itself. If we want to chain these methods, we need to add them to the relation, and not to the repository. Let’s create similar methods over in <code>app/relations/books.rb</code> now:</p>

<pre><code class="language-ruby">def by_year(year)
  where(year: year)
end

def by_author(author)
  where(author: author)
end
</code></pre>

<p>We can now use these methods, rather than defining the same logic again, back in the repository. Let’s change the code there in <code>app/repos/book_repo.rb</code> to this:</p>

<pre><code class="language-ruby">def by_year(year)
  books.by_year(year)
end

def by_author(author)
  books.by_author(author)
end
</code></pre>

<p>By moving these methods over to the relation, we should now be able to chain them together. Let’s reload the console and try again:</p>

<pre><code class="language-ruby">book_repo = app["repos.book_repo"]
book_repo.by_year(2025).by_author("Ryan Bigg")
</code></pre>

<p>What we get back here is a new instance of <code>Bookshelf::Relations::Books</code>, because we haven’t asked this relation to do any more than to generate us a query based on books for a particular year and author. At this point, we <em>could</em> throw some more <code>where</code> clauses onto the end if we wanted to further scope the data.</p>

<p>We can trigger a query to run by asking this for the <em>first</em> book.</p>

<pre><code class="language-ruby">book_repo = app["repos.book_repo"]
book_repo.by_year(2025).by_author("Ryan Bigg").first
</code></pre>

<p>This returns nothing! This is because there is no book with that year in our dataset, we only created a book with a title and an author, not a year. We can update our record to have a year by running:</p>

<pre><code class="language-ruby">book_repo.books.where(id: 1).update(year: 2025)
</code></pre>

<p>Instead of doing a <code>find</code> then an <code>update</code> like you might in a Rails app, we’re doing only an update. That’s all we need to do here. Let’s try running that query again to get the first book:</p>

<pre><code class="language-ruby">book_repo = app["repos.book_repo"]
book_repo.by_year(2025).by_author("Ryan Bigg").first
=&gt; #&lt;Bookshelf::Structs::Book id=1 title="Hanami for Rails Developers" author="Ryan Bigg" year=2025&gt;
</code></pre>

<p>Great!</p>

<p>As we can see from this “Model Layer” section of this guide, Hanami provides three distinct layers of separation here:</p>

<ol>
  <li><strong>Repositories</strong>: Defines the interactions between your database and your application.</li>
  <li><strong>Relations</strong>: Provides a home for your application’s complicated queries.</li>
  <li><strong>Structs</strong>: Represents rows from your database in plain and simple Ruby objects.</li>
</ol>

<p>Rails would have you throw all of this into the one class (a model), leading to quite a lot of mess and making things harder to read. Hanami’s separation is initially disorienting (which file was that code in?) but after a few days that disorientation will wear off!</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This blog post is part of a series called “Hanami for Rails Developers”.]]></summary></entry><entry><title type="html">Hanami for Rails Developers: Part 2: Controllers</title><link href="https://ryanbigg.com/2025/10/hanami-for-rails-developers-2-controllers" rel="alternate" type="text/html" title="Hanami for Rails Developers: Part 2: Controllers" /><published>2025-10-05T00:00:00+00:00</published><updated>2025-10-05T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/10/hanami-for-rails-developers-2-controllers</id><content type="html" xml:base="https://ryanbigg.com/2025/10/hanami-for-rails-developers-2-controllers"><![CDATA[<p>This blog post is part of a series called “Hanami for Rails Developers”.</p>

<ul>
  <li>Part 1: <a href="/2025/10/hanami-for-rails-developers-1-models">Models</a></li>
  <li>Part 2: <a href="/2025/10/hanami-for-rails-developers-2-controllers">Controllers</a> (you are here)</li>
  <li>Part 3: <a href="/2025/10/hanami-for-rails-developers-3-forms">Forms</a></li>
  <li>Part 4: <a href="/2025/10/hanami-for-rails-developers-4-associations">Associations</a></li>
</ul>

<p>In the first part we saw how to interact with a database by using Hanami’s repositories and relations. In this part, we continue that by serving that data out through routes of our Hanami application.</p>

<p>To get started here, we can run the Hanami server (and its asset compilation step) by running:</p>

<pre><code>hanami dev
</code></pre>

<p>This will run a server on localhost:2300 and once you come back to the browser to figure out why your muscle-memory’d localhost:3000 didn’t work, change that 3000 to a 2300.</p>

<h3 id="routing">Routing</h3>

<p>In a Hanami application, you can find the routes in the familiar location of <code>config/routes.rb</code>. We can add a route to this application by changing this file to this code:</p>

<pre><code class="language-ruby">module Bookshelf
  class Routes &lt; Hanami::Routes
    root to: "books.index"
  end
end
</code></pre>

<p>Note that the code here uses a dot to separate the controller and the action, rather than a hash/pound-sign (#).</p>

<p>A route by itself, like in a Rails app, doesn’t do very much. We need a matching action for this.</p>

<h3 id="actions">Actions</h3>

<p>We generate an action in Hanami by running:</p>

<pre><code>hanami g action books.index
</code></pre>

<p>This time, I will list the files this generates, as this a key part where Hanami differentiates itself from Rails:</p>

<pre><code>Updated config/routes.rb
Created app/actions/books/
Created app/actions/books/index.rb
Created app/views/books/
Created app/views/books/index.rb
Created app/templates/books/
Created app/templates/books/index.html.erb
Created spec/actions/books/index_spec.rb
</code></pre>

<p>This has updated our <code>config/routes.rb</code> file to include a new <code>/books</code> route:</p>

<pre><code class="language-ruby">get "/books", to: "books.index"
</code></pre>

<p>Classes in Hanami applications are namespaced automatically under the application’s name. You can see this by looking at the two classes generated for us here which are both created under the <code>Bookshelf</code> namespace: <code>Actions::Books::Index</code>, and <code>Views::Books::Index</code>.</p>

<p>Hanami has no controllers, and instead splits this logic between two classes: <strong>actions</strong> and <strong>views</strong>.</p>

<p>The purpose of actions is to handle all the parameter parsing and response handling of a request. This is where you might also put behavior like authenticating or authorizing a user before they can perform this particular action. An action can decide based on these parameters to render either the default view, or a different one. An action in Hanami can also validate the input parameters before deciding to proceed with the action.</p>

<p>The purpose of views is to gather up and present the data once an action has decided which version of a view to render. In a Rails app, you may see similar handling by way of <code>respond_to</code>.</p>

<h3 id="views">Views</h3>

<p>Views typically have a template to render as well, and in this application we now have <code>app/templates/books/index.html.erb</code>. This is the same kind of file you’d get with Rails, only in Rails it would be under <code>app/views</code>. Views in Hanami have a different meaning, and that can take some time to get your head around.</p>

<p>At the moment, requests to http://localhost:2300/books shows very little, just a big H1 showing: <code>Bookshelf::Views::Books::Index</code>. This isn’t going to drive engagement for our book application. We’ll add some books to this page instead, by fetching them from the database and displaying them here.</p>

<p>To fetch these books from the database, we will open <code>app/views/books/index.rb</code> and fetch all the books with this code:</p>

<pre><code class="language-ruby">module Bookshelf
  module Views
    module Books
      class Index &lt; Bookshelf::View
        include Deps["repos.book_repo"]

        expose :books do
          book_repo.all
        end
      end
    end
  end
end
</code></pre>

<p>When coming from a Rails application where it is almost forbidden (but possible!) to put a database query in a view, it might feel weird to put a database call into a class with “Views” in the name.</p>

<p>In Hanami, we put the database loading in the view because the action might have had a reason to not need to load all the books, such as if there was an authorization rule on the action that was blocking the request.</p>

<p>At the top of this view, we include the book repository as a dependency by using <code>include</code>. This makes it explicit what external dependencies this view has, right at the top of the file.</p>

<p>In a Hanami view, we expose the data to the view explicitly with the use of <code>expose</code>, rather than defining an instance variable and it magically appearing in the template. The <code>book_repo</code> method here comes from the earlier <code>include</code>, and it will be an instantiated version of the <code>Repos::BookRepo</code> class.</p>

<p>Speaking of templates, we can display these books from our database by writing some ERB code. This will land us in well familiar territory. The template for this action lives at <code>app/templates/books/index.html.erb</code>. We’ll remove all the content in this file, and replace it with our own:</p>

<pre><code class="language-erb">&lt;h1&gt;Books&lt;/h1&gt;

&lt;% books.each do |book| %&gt;
  &lt;div&gt;
    &lt;h2&gt;&lt;%= book.title %&gt;&lt;/h2&gt;
    &lt;p&gt;Author: &lt;%= book.author %&gt;&lt;/p&gt;
    &lt;p&gt;Year: &lt;%= book.year %&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;% end %&gt;
</code></pre>

<p>When we refresh this page, we’ll now see our book coming back:</p>

<p><img src="/images/hanami/books_index.jpg" alt="Books" /></p>

<p>We’re now able to display a list of books, but let’s look at how we can display books from a given year.</p>

<h3 id="working-with-parameters">Working with parameters</h3>

<p>In this Hanami application, we would like a route at <code>/books/year/2025</code> to return only the books from that specified year. Let’s add that route to the <code>config/routes.rb</code> file in our application now:</p>

<pre><code class="language-ruby">get "/books/year/:year", to: "books.index"
</code></pre>

<p>This action will route to the <code>index</code> action, the same as our previous route. To make this action behave differently based on if we’re asking for <em>all books</em> or <em>all books for a particular year</em>, we’re going to update the action’s code in <code>app/actions/books/index.rb</code> to this:</p>

<pre><code class="language-ruby">module Bookshelf
  module Actions
    module Books
      class Index &lt; Bookshelf::Action
        include Deps[
          books_index: "views.books.index",
          books_by_year: "views.books.by_year"
        ]

        def handle(request, response)
          if request.params[:year]
            response.render(books_by_year, year: request.params[:year])
          else
            response.render(books_index)
          end
        end
      end
    end
  end
end

</code></pre>

<p>We’re again importing dependencies into this action, this time some instances of our relative views. If the <code>year</code> parameter is specified, we’re going to render the <code>books_by_year</code> view, passing it the <code>year</code> parameter.</p>

<p>If the parameter isn’t set, we’ll render <code>books_index</code>, which will show us the list of all books.</p>

<p>The <code>books.by_year</code> view doesn’t exist yet, so let’s create it:</p>

<pre><code>hanami g view books.by_year
</code></pre>

<p>In this view, we’ll want to fetch all the books for a particular year. We can do this with this code:</p>

<pre><code class="language-ruby">module Bookshelf
  module Views
    module Books
      class ByYear &lt; Bookshelf::View
        include Deps["repos.book_repo"]

        expose :books do |year:|
          book_repo.by_year(year).to_a
        end

        expose :year
      end
    end
  end
end
</code></pre>

<p>The block used in <code>expose</code> take in the parameter passed in from the controller and display us a list of books from that year. As we’ll want to expose the year itself to our view, we need to explicitly call that out in the view too.</p>

<p>In the matching template for this view, <code>app/templates/books/by_year.html.erb</code>, we’ll add this code:</p>

<pre><code class="language-ruby">&lt;h1&gt;Books from &lt;%= year %&gt;&lt;/h1&gt;

&lt;% books.each do |book| %&gt;
  &lt;div&gt;
    &lt;h2&gt;&lt;%= book.title %&gt;&lt;/h2&gt;
    &lt;p&gt;Author: &lt;%= book.author %&gt;&lt;/p&gt;
  &lt;/div&gt;
&lt;% end %&gt;
</code></pre>

<p>This view will now display a list of books from 2025 when we go to http://localhost:2300/books/year/2025.</p>

<p><img src="/images/hanami/books_by_year.jpg" alt="Books by year" /></p>

<p>We’ve now added two ways to use the same action, with two different views. In a RESTful application, we would typically have more actions than this. You’d be familiar with the set of them from a Rails application:</p>

<ul>
  <li>index</li>
  <li>show</li>
  <li>new</li>
  <li>create</li>
  <li>edit</li>
  <li>update</li>
  <li>destroy</li>
</ul>

<p>In the remainder of this part, we’ll cover off the show action. We’ll leave the forms to the next part of this guide.</p>

<h3 id="adding-a-show-route">Adding a show route</h3>

<p>We’re now going to add a <code>show</code> action to our application, allowing us to display information about a single book. When we add this route, we will also add a link from our books “index” actions to the show action. Rather than starting with the route, we’ll start with generating an action:</p>

<pre><code class="language-ruby">hanami g action books.show
</code></pre>

<p>Hanami is smart enough to generate us an action <em>and</em> a route with this command. Here’s what it has added to <code>config/routes.rb</code>:</p>

<pre><code class="language-ruby">get "/books/:id", to: "books.show"
</code></pre>

<p>This route is exactly the kind of route you’d get with a Rails application. With one key difference: we don’t yet have a named way to refer to this route. In Hanami, we can give routes names using <code>as:</code>. Let’s make that change in our routes now:</p>

<pre><code class="language-ruby">get "/books/:id", to: "books.show", as: :book
</code></pre>

<p>To send our users to this page, we need to create a link from there to the show page. Let’s open up <code>app/templates/books/index.html.erb</code> and change this line:</p>

<pre><code class="language-erb">&lt;h2&gt;&lt;%= book.title %&gt;&lt;/h2&gt;
</code></pre>

<p>To this:</p>

<pre><code class="language-erb">&lt;h2&gt;&lt;%= link_to book.title, routes.path(:book, id: book.id) %&gt;&lt;/h2&gt;
</code></pre>

<p>Let’s also make this same change in <code>app/templates/books/by_year.html.erb</code> too.</p>

<p>Routing methods in Hanami aren’t dynamically generated like in Rails, and so we need to write these out in a slightly longer format.</p>

<p>Now that we have a route, we need to display some information on the page where this route goes to. We’ll need to pull that information out of the database before we can display it. Let’s go over to our <code>Books::Show</code> action in <code>app/actions/books/show.rb</code>, and pass down the <code>id</code> parameter to the view:</p>

<pre><code class="language-ruby">module Bookshelf
  module Actions
    module Books
      class Show &lt; Bookshelf::Action
        def handle(request, response)
          response.render(view, id: request.params[:id])
        end
      end
    end
  end
end
</code></pre>

<p>Rather than views instantly getting access to all parameters, we must expose these from the action first. We can pass these in with <code>response.render(view, ...)</code>, as this will render the default view for this action.</p>

<p>To then make the view fetch this book from the database, we’ll make these changes in <code>app/views/books/show.rb</code>:</p>

<pre><code class="language-ruby">module Bookshelf
  module Views
    module Books
      class Show &lt; Bookshelf::View
        include Deps["repos.book_repo"]

        expose :book do |id:|
          book_repo.find(id)
        end
      end
    end
  end
end
</code></pre>

<p>This view is now using the book repository to find the book with that ID. When it finds that book, it’ll expose the book to the template. Let’s use that to display information about the book now in <code>app/templates/books/show.html.erb</code>:</p>

<pre><code class="language-erb">&lt;h1&gt;&lt;%= book.title %&gt;&lt;/h1&gt;

&lt;p&gt;Author: &lt;%= book.author %&gt;&lt;/p&gt;
&lt;p&gt;Year: &lt;%= book.year %&gt;&lt;/p&gt;
</code></pre>

<h3 id="parts---hanamis-decorators">Parts - Hanami’s decorators</h3>

<p>Writing these routes out in longer form is going to get tiring after a while. Fortunately for us, Hanami provides a location where we can add methods that decorate the objects that we use in a view.</p>

<p>When we <code>expose</code> data from an action, Hanami wraps this data in another class, which it calls a Part. In the case of the <code>expose :books</code> that we have, it will wrap these in two distinct parts:</p>

<ul>
  <li><code>Views::Parts::Books</code> - for the whole array of books</li>
  <li><code>Views::Parts::Book</code> - one wrapping for each of the books</li>
</ul>

<p>We didn’t create these classes. Hanami did that for us. Hanami uses the class of the struct to determine which part to use.</p>

<p>We can define these classes ourselves if we want to add decorations to the objects exposed here. A good example of this would be to add a <code>show_path</code> method to books, so that we don’t have to write out the route long-form all the time.</p>

<p>We can create a new class at <code>app/views/parts/book.rb</code> and define this method inside:</p>

<pre><code class="language-ruby">module Bookshelf
  module Views
    module Parts
      class Book &lt; Bookshelf::Views::Part
        def show_path
          context.routes.path(:book, id: id)
        end
      end
    end
  end
end
</code></pre>

<p>Methods of this class act as though they’re defined as instance methods on <code>Book</code>. This works because in the view we’re actually working with <code>Views::Parts::Book</code>, rather than a straight <code>Bookshelf::Structs::Book</code> instance. The <code>context</code> used here is the Hanami view rendering context, which we use to get to the <code>routes</code> method.</p>

<p>By defining this <code>show_path</code> this way, we can now change our links in <code>app/templates/books/index.html.erb</code> and <code>app/templates/books/by_year.html.erb</code> to simply this:</p>

<pre><code class="language-ruby">&lt;h2&gt;&lt;%= link_to book.title, book.show_path %&gt;&lt;/h2&gt;
</code></pre>

<p>The great thing about this is that if we ever want to know where <code>show_path</code> is defined, we can simply do a find in our codebase for this method, and it will turn up the part. Contrast that to Rails’ dynamic routing methods, and you’ll see that this a vast improvement.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[This blog post is part of a series called “Hanami for Rails Developers”.]]></summary></entry><entry><title type="html">Show, Don’t Tell</title><link href="https://ryanbigg.com/2025/05/show-dont-tell" rel="alternate" type="text/html" title="Show, Don’t Tell" /><published>2025-05-03T00:00:00+00:00</published><updated>2025-05-03T00:00:00+00:00</updated><id>https://ryanbigg.com/2025/05/show-dont-tell</id><content type="html" xml:base="https://ryanbigg.com/2025/05/show-dont-tell"><![CDATA[<p>On Monday night, I’m going to be on a panel in Melbourne, in front of a crowd of aspirational junior developers, answering questions and giving advice. I’ve been <a href="https://ryanbigg.com/2018/03/hiring-juniors">a proponent for junior developers for a very long time</a>, and ran two successful iterations of my Junior Engineering Program at Culture Amp, ending in 2019, as well as continuing to mentor developers in my current line of work.</p>

<p>My advice to the juniors of 2025 is plain and simple: <strong>Show, Don’t Tell.</strong> The first time I hear from a lot of juniors is probably when they apply for a job, or reach out about one. It used to be meetups but the tyranny of distance got in the way.</p>

<p>When they reach out, that’s when I’ll find out the regular things of what tools they’ve used. HTML, CSS, JavaScript, some framework or another. Catch me on a good day (most of them) and I’ll even take a look at their GitHub profiles and portfolios. I’m a curious sort of guy.</p>

<p>The ones that stand out the most do a really great job of <em>showing</em> me that they know the tools, and that they’ve gone past a first tutorial stage.</p>

<ul>
  <li>A React app that ranks your favourite books, then orders them by read date, then reorders them by cover colour.</li>
  <li>A game you made because you had an idea you couldn’t leave behind. Yes, even if the game is naff.</li>
  <li>Show me a thing I didn’t think CSS could do, ever.</li>
</ul>

<p>All of this goes a long way to showing me an aptitude that already puts you ahead of 90% of the competition. These are the outliers I will notice and think more about.</p>

<p>So: <em>Show me</em> what you can do, rather than giving me a list of tools. A Luthier and I both know how to use a saw, but only one of us knows how to make a guitar. The proof is in the doing, not the telling.</p>

<p><small>(And for god sake: use a colour other than black and white on your resumé!)</small></p>]]></content><author><name></name></author><summary type="html"><![CDATA[On Monday night, I’m going to be on a panel in Melbourne, in front of a crowd of aspirational junior developers, answering questions and giving advice. I’ve been a proponent for junior developers for a very long time, and ran two successful iterations of my Junior Engineering Program at Culture Amp, ending in 2019, as well as continuing to mentor developers in my current line of work.]]></summary></entry></feed>