<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">

  <title><![CDATA[Andy Croll]]></title>
  <link href="https://andycroll.com/index.xml" rel="self"/>
  <link href="https://andycroll.com/"/>
  <updated>2026-05-18T08:58:05+00:00</updated>
  <id>https://andycroll.com/</id>
  <author>
    <name><![CDATA[Andy Croll]]></name>
    <email><![CDATA[andy@goodscary.com]]></email>
  </author>
  <generator uri="http://jekyllrb.com/">Jekyll</generator>

  
  <entry>
    <title type="html"><![CDATA[Use class_names to Conditionally Apply CSS Classes]]></title>
    <link href="https://andycroll.com/ruby/use-class-names-to-conditionally-apply-css-classes/"/>
    <updated>2026-05-11T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/use-class-names-to-conditionally-apply-css-classes</id>
    <content type="html"><![CDATA[<p>When you’re building views in Rails, you often need to apply CSS classes conditionally. Maybe a nav link should look different when it’s the current page, or a form field needs error styling. Since Rails 6.1, the <a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html#method-i-class_names"><code class="language-plaintext highlighter-rouge">class_names</code></a> helper does this cleanly.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…interpolating conditional classes with ternaries or post-statement conditionals:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"p-4 rounded </span><span class="cp">&lt;%=</span> <span class="vi">@error</span> <span class="p">?</span> <span class="s1">'bg-red-50 border-red-500'</span> <span class="p">:</span> <span class="s1">''</span> <span class="cp">%&gt;</span><span class="s"> </span><span class="cp">&lt;%=</span> <span class="s1">'opacity-50 cursor-not-allowed'</span> <span class="k">if</span> <span class="vi">@disabled</span> <span class="cp">%&gt;</span><span class="s">"</span><span class="nt">&gt;</span>
  <span class="cp">&lt;%=</span> <span class="vi">@message</span> <span class="cp">%&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the <code class="language-plaintext highlighter-rouge">class_names</code> helper:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">div</span> <span class="ss">class: </span><span class="n">class_names</span><span class="p">(</span>
  <span class="s2">"p-4 rounded"</span><span class="p">,</span>
  <span class="s2">"bg-red-50 border-red-500"</span><span class="p">:</span> <span class="vi">@error</span><span class="p">,</span>
  <span class="s2">"opacity-50 cursor-not-allowed"</span><span class="p">:</span> <span class="vi">@disabled</span>
<span class="p">)</span> <span class="k">do</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="vi">@message</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">String</code> arguments are always applied; trailing keyword-style entries are included when their value is truthy and silently dropped otherwise.</p>

<p>Better still — Rails tag helpers (<code class="language-plaintext highlighter-rouge">tag.*</code>, <code class="language-plaintext highlighter-rouge">link_to</code>, form builders) already run the <code class="language-plaintext highlighter-rouge">class:</code> argument through the process implicitly, so you can drop the wrapper and hand it an array directly. The trailing <code class="language-plaintext highlighter-rouge">key: value</code> pairs don’t need their own <code class="language-plaintext highlighter-rouge">{}</code> either; Ruby wraps them into a <code class="language-plaintext highlighter-rouge">Hash</code> for you:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">div</span> <span class="ss">class: </span><span class="p">[</span>
  <span class="s2">"p-4 rounded"</span><span class="p">,</span>
  <span class="s2">"bg-red-50 border-red-500"</span><span class="p">:</span> <span class="vi">@error</span><span class="p">,</span>
  <span class="s2">"opacity-50 cursor-not-allowed"</span><span class="p">:</span> <span class="vi">@disabled</span>
<span class="p">]</span> <span class="k">do</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="vi">@message</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>An active nav link:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">link_to</span> <span class="s2">"Home"</span><span class="p">,</span> <span class="n">root_path</span><span class="p">,</span>
  <span class="ss">class: </span><span class="p">[</span><span class="s2">"nav-link px-3 py-2"</span><span class="p">,</span>
    <span class="s2">"text-blue-700 font-semibold"</span><span class="p">:</span> <span class="n">current_page?</span><span class="p">(</span><span class="n">root_path</span><span class="p">)]</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>A form field with errors:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">f</span><span class="p">.</span><span class="nf">text_field</span> <span class="ss">:email</span><span class="p">,</span>
  <span class="ss">class: </span><span class="p">[</span><span class="s2">"field"</span><span class="p">,</span> <span class="s2">"field--error"</span><span class="p">:</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">errors</span><span class="p">[</span><span class="ss">:email</span><span class="p">].</span><span class="nf">any?</span><span class="p">]</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>A flash message:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%</span> <span class="n">flash</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">type</span><span class="p">,</span> <span class="n">message</span><span class="o">|</span> <span class="cp">%&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">p</span> <span class="n">message</span><span class="p">,</span> <span class="ss">class: </span><span class="p">[</span><span class="s2">"flash"</span><span class="p">,</span>
    <span class="ss">notice: </span><span class="n">type</span> <span class="o">==</span> <span class="s2">"notice"</span><span class="p">,</span>
    <span class="ss">alert: </span><span class="n">type</span> <span class="o">==</span> <span class="s2">"alert"</span><span class="p">]</span> <span class="cp">%&gt;</span>
<span class="cp">&lt;%</span> <span class="k">end</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>An active tab:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">link_to</span> <span class="s2">"Overview"</span><span class="p">,</span> <span class="n">project_path</span><span class="p">(</span><span class="vi">@project</span><span class="p">),</span>
  <span class="ss">class: </span><span class="p">[</span><span class="s2">"tab"</span><span class="p">,</span> <span class="s2">"tab--active"</span><span class="p">:</span> <span class="n">current_page?</span><span class="p">(</span><span class="n">project_path</span><span class="p">(</span><span class="vi">@project</span><span class="p">))]</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>Or wrap a repeated pattern in a helper. Helpers often return <code class="language-plaintext highlighter-rouge">String</code>s, which would be a case where you call <code class="language-plaintext highlighter-rouge">class_names</code> directly:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">class_names_for_project</span><span class="p">(</span><span class="n">project</span><span class="p">)</span>
  <span class="n">class_names</span><span class="p">(</span><span class="s2">"status-badge"</span><span class="p">,</span>
    <span class="s2">"status-badge--primary"</span><span class="p">:</span> <span class="n">project</span><span class="p">.</span><span class="nf">active?</span><span class="p">,</span>
    <span class="s2">"status-badge--muted"</span><span class="p">:</span> <span class="n">project</span><span class="p">.</span><span class="nf">archived?</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">span</span> <span class="vi">@project</span><span class="p">.</span><span class="nf">status</span><span class="p">,</span> <span class="ss">class: </span><span class="n">class_names_for_project</span><span class="p">(</span><span class="vi">@project</span><span class="p">)</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>The unsophisticated approach ends up with extra whitespace in the rendered HTML with ERB tags inside an HTML attribute (which I’m not a fan of visually), plus it’s hard to scan which classes are always present and which are conditional.</p>

<p>The tag helpers call <code class="language-plaintext highlighter-rouge">token_list</code> on whatever you pass to <code class="language-plaintext highlighter-rouge">class:</code>, which is aliased as <code class="language-plaintext highlighter-rouge">class_names</code>. It splits whitespace-separated tokens, deduplicates them, and returns an HTML-safe string. So <code class="language-plaintext highlighter-rouge">["p-4", "p-4 rounded"]</code> collapses to <code class="language-plaintext highlighter-rouge">"p-4 rounded"</code> rather than repeating <code class="language-plaintext highlighter-rouge">p-4</code>.</p>

<p>You have to call <code class="language-plaintext highlighter-rouge">class_names</code> directly when you’re not inside a tag helper — building a string in a helper method, or interpolating into raw HTML. It’s available in all views and helpers in Rails since it’s defined in <code class="language-plaintext highlighter-rouge">ActionView::Helpers::TagHelper</code>. You might also see it referred to as <code class="language-plaintext highlighter-rouge">token_list</code>, which is the original method name.</p>

<h2 id="why-not">Why not?</h2>

<p>If you’ve only got a single conditional class, plain ERB is readable enough:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"p-4 </span><span class="cp">&lt;%=</span> <span class="s1">'font-bold'</span> <span class="k">if</span> <span class="vi">@important</span> <span class="cp">%&gt;</span><span class="s">"</span><span class="nt">&gt;</span>
</code></pre></div></div>

<p>Though the array form does avoid the awkward whitespace issue when the condition is false:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">tag</span><span class="p">.</span><span class="nf">div</span> <span class="ss">class: </span><span class="p">[</span><span class="s2">"p-4"</span><span class="p">,</span> <span class="s2">"font-bold"</span><span class="p">:</span> <span class="vi">@important</span><span class="p">]</span> <span class="k">do</span> <span class="cp">%&gt;</span>
</code></pre></div></div>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Avoid html_safe with Tag Helpers, safe_join, and sanitize]]></title>
    <link href="https://andycroll.com/ruby/alternatives-to-html-safe-in-rails/"/>
    <updated>2026-05-04T09:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/alternatives-to-html-safe-in-rails</id>
    <content type="html"><![CDATA[<p>When you need to build HTML outside of a template, it’s tempting to concatenate strings and call <code class="language-plaintext highlighter-rouge">html_safe</code> on the result. This bypasses Rails’s built-in <a href="/ruby/beware-of-raw-erb/">XSS protection</a> entirely: any user input in that string goes straight to the browser unescaped.</p>

<p>The good news is you almost never need <code class="language-plaintext highlighter-rouge">html_safe</code>. Rails provides three underappreciated tools that handle escaping for you.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…calling <code class="language-plaintext highlighter-rouge">html_safe</code> on strings you’ve built by hand:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">status_badge</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">color</span><span class="p">)</span>
  <span class="s2">"&lt;span class=</span><span class="se">\"</span><span class="s2">badge badge-</span><span class="si">#{</span><span class="n">color</span><span class="si">}</span><span class="se">\"</span><span class="s2">&gt;</span><span class="si">#{</span><span class="n">label</span><span class="si">}</span><span class="s2">&lt;/span&gt;"</span><span class="p">.</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">formatted_address</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="p">[</span><span class="n">user</span><span class="p">.</span><span class="nf">street</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">city</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">postcode</span><span class="p">].</span><span class="nf">compact</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s2">"&lt;br&gt;"</span><span class="p">).</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">render_comment</span><span class="p">(</span><span class="n">comment</span><span class="p">)</span>
  <span class="n">comment</span><span class="p">.</span><span class="nf">body_html</span><span class="p">.</span><span class="nf">html_safe</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the right tool for each situation.</p>

<p>When you need to <strong>build HTML elements</strong>, use <code class="language-plaintext highlighter-rouge">tag</code> helpers:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">status_badge</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="n">color</span><span class="p">)</span>
  <span class="n">tag</span><span class="p">.</span><span class="nf">span</span><span class="p">(</span><span class="n">label</span><span class="p">,</span> <span class="ss">class: </span><span class="s2">"badge badge-</span><span class="si">#{</span><span class="n">color</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p>The <a href="https://api.rubyonrails.org/classes/ActionView/Helpers/TagHelper.html"><code class="language-plaintext highlighter-rouge">tag</code> helper</a> escapes the content and attributes automatically. It returns an HTML-safe string without you having to think about it.</p>

<p>When you need to <strong>join fragments</strong> that mix safe HTML with potentially unsafe text, use <code class="language-plaintext highlighter-rouge">safe_join</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">formatted_address</span><span class="p">(</span><span class="n">user</span><span class="p">)</span>
  <span class="n">safe_join</span><span class="p">([</span><span class="n">user</span><span class="p">.</span><span class="nf">street</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">city</span><span class="p">,</span> <span class="n">user</span><span class="p">.</span><span class="nf">postcode</span><span class="p">].</span><span class="nf">compact</span><span class="p">,</span> <span class="n">tag</span><span class="p">.</span><span class="nf">br</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p><a href="https://api.rubyonrails.org/classes/ActionView/Helpers/OutputSafetyHelper.html#method-i-safe_join"><code class="language-plaintext highlighter-rouge">safe_join</code></a> escapes any unsafe strings in the array and returns an HTML-safe result. It’s <code class="language-plaintext highlighter-rouge">Array#join</code> with protection built in.</p>

<p>When you need to <strong>accept user-provided HTML</strong> but strip dangerous tags, use <code class="language-plaintext highlighter-rouge">sanitize</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">render_comment</span><span class="p">(</span><span class="n">comment</span><span class="p">)</span>
  <span class="n">sanitize</span><span class="p">(</span><span class="n">comment</span><span class="p">.</span><span class="nf">body_html</span><span class="p">)</span>
<span class="k">end</span>
</code></pre></div></div>

<p><a href="https://api.rubyonrails.org/classes/ActionView/Helpers/SanitizeHelper.html#method-i-sanitize"><code class="language-plaintext highlighter-rouge">sanitize</code></a> keeps safe tags like <code class="language-plaintext highlighter-rouge">&lt;p&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;strong&gt;</code>, and <code class="language-plaintext highlighter-rouge">&lt;em&gt;</code> while stripping <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code>, event handlers, and other XSS vectors. You can customise the allowed tags and attributes, but beware of straying past the defaults — they are battle-tested and loosening them is a <a href="https://en.wiktionary.org/wiki/footgun">footgun</a>.</p>

<h2 id="why">Why?</h2>

<p>Each of these tools lets Rails manage HTML safety for you. You describe what you want — an element, a joined list, sanitised content — and the framework handles the escaping.</p>

<p><code class="language-plaintext highlighter-rouge">html_safe</code> does the opposite. It tells Rails “trust this string, don’t escape it”. That’s a promise <em>you</em> have to keep, and it’s easy to break when the inputs change or a future developer doesn’t realise user data flows through that path. Ask your friendly security consultant or penetration testing organisation why this is a bad idea.</p>

<p>The mental model is simple. Need an HTML element? <code class="language-plaintext highlighter-rouge">tag</code>. Joining fragments? <code class="language-plaintext highlighter-rouge">safe_join</code>. Accepting rich text? <code class="language-plaintext highlighter-rouge">sanitize</code>. If none of those fit, you probably need a partial or a component, not a string.</p>

<h2 id="why-not">Why not?</h2>

<p>There are legitimate uses of <code class="language-plaintext highlighter-rouge">html_safe</code>. Some gems, like <a href="https://github.com/ddnexus/pagy"><code class="language-plaintext highlighter-rouge">pagy</code></a>, return pre-built HTML strings that are safe by construction. Calling <code class="language-plaintext highlighter-rouge">html_safe</code> on their output is fine because the gem controls the content.</p>

<p>You might also see <code class="language-plaintext highlighter-rouge">html_safe</code> on strings that are genuinely static with no user input, like <code class="language-plaintext highlighter-rouge">"&amp;nbsp;".html_safe</code>. That’s harmless, but you can include the actual Unicode character instead — <code class="language-plaintext highlighter-rouge">"\u00A0"</code> gives you a non-breaking space without needing <code class="language-plaintext highlighter-rouge">html_safe</code> at all. That is ugly as hell though, so it’s your call!</p>

<p>The key question is always: could user input end up in this string? If the answer is yes, or even <em>maybe</em>, reach for <code class="language-plaintext highlighter-rouge">tag</code>, <code class="language-plaintext highlighter-rouge">safe_join</code>, or <code class="language-plaintext highlighter-rouge">sanitize</code> instead.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Use Rails Combined Credentials]]></title>
    <link href="https://andycroll.com/ruby/use-rails-combined-credentials/"/>
    <updated>2026-04-13T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/use-rails-combined-credentials</id>
    <content type="html"><![CDATA[<p>To deal with secrets and credential handling most Rails apps have ended up with a hotchpotch of <code class="language-plaintext highlighter-rouge">ENV.fetch</code> calls and <code class="language-plaintext highlighter-rouge">credentials.dig</code> lookups throughout the codebase, depending on where each secret lives.</p>

<p>Rails edge — and the upcoming 8.2 — fixes this.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…mixing ENV and credential lookups:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeChargeService</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@api_key</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"STRIPE_API_KEY"</span><span class="p">)</span>
    <span class="vi">@webhook_secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:webhook_secret</span><span class="p">)</span>
    <span class="vi">@price_id</span> <span class="o">=</span> <span class="no">ENV</span><span class="p">.</span><span class="nf">fetch</span><span class="p">(</span><span class="s2">"STRIPE_PRICE_ID"</span><span class="p">)</span> <span class="p">{</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:price_id</span><span class="p">)</span> <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…the combined credentials API:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">StripeChargeService</span>
  <span class="k">def</span> <span class="nf">initialize</span>
    <span class="vi">@api_key</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:api_key</span><span class="p">)</span>
    <span class="vi">@webhook_secret</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:webhook_secret</span><span class="p">)</span>
    <span class="vi">@price_id</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">require</span><span class="p">(</span><span class="ss">:stripe</span><span class="p">,</span> <span class="ss">:price_id</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">require</code> raises a <code class="language-plaintext highlighter-rouge">KeyError</code> if the key is missing from all backends. For optional values, use <code class="language-plaintext highlighter-rouge">option</code>:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">Rails</span><span class="p">.</span><span class="nf">app</span><span class="p">.</span><span class="nf">creds</span><span class="p">.</span><span class="nf">option</span><span class="p">(</span><span class="ss">:appsignal</span><span class="p">,</span> <span class="ss">:push_api_key</span><span class="p">,</span> <span class="ss">default: </span><span class="kp">nil</span><span class="p">)</span>
<span class="c1"># Returns nil if missing — AppSignal just won't report</span>
</code></pre></div></div>

<p>To keep production secrets separate, run <code class="language-plaintext highlighter-rouge">bin/rails credentials:edit --environment production</code>. This creates a separate encrypted file with its own key:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>config/credentials.yml.enc         ← shared (dev/test)
config/master.key                  ← decrypts the shared file

config/credentials/production.yml.enc  ← production only
config/credentials/production.key      ← decrypts production
</code></pre></div></div>

<p>If <code class="language-plaintext highlighter-rouge">production.yml.enc</code> exists, Rails uses it exclusively in production — there’s no inheritance from the shared file, so duplicate any keys you need. To decrypt in production, set <code class="language-plaintext highlighter-rouge">RAILS_MASTER_KEY</code> in your hosting provider to the contents of <code class="language-plaintext highlighter-rouge">production.key</code>.</p>

<h2 id="why">Why?</h2>

<p><code class="language-plaintext highlighter-rouge">Rails.app.creds</code> checks ENV first, then falls back to encrypted credentials. You don’t need to know or care where a value is stored.</p>

<p>Nested keys like <code class="language-plaintext highlighter-rouge">:stripe, :api_key</code> map to double-underscored ENV names (<code class="language-plaintext highlighter-rouge">STRIPE__API_KEY</code>). A single key like <code class="language-plaintext highlighter-rouge">:postmark_api_token</code> checks <code class="language-plaintext highlighter-rouge">ENV["POSTMARK_API_TOKEN"]</code>.</p>

<p>This means you can move secrets between ENV and encrypted credentials without changing application code. Deploying to a provider that injects secrets via ENV? It just works. Want to move a key into the encrypted file instead? Remove the ENV variable and add it to your credentials. Your code stays the same.</p>

<p>I’ve <a href="/ruby/wrap-your-environment-variables-in-a-settings-object/">previously recommended</a> wrapping ENV in a custom Settings object. This built-in approach is better — the same clean interface with the added fallback to encrypted credentials.</p>

<h2 id="why-not">Why not?</h2>

<p>This isn’t in a released version of Rails yet — you need Rails edge (<code class="language-plaintext highlighter-rouge">main</code>), and it’s expected in Rails 8.2. If you’re on 8.1 or older, a <a href="/ruby/wrap-your-environment-variables-in-a-settings-object/">custom Settings wrapper</a> still works well.</p>

<h3 id="other-considerations">Other Considerations</h3>

<p>You can also create <code class="language-plaintext highlighter-rouge">development.yml.enc</code> and <code class="language-plaintext highlighter-rouge">test.yml.enc</code>, but I think the shared file plus a production override is clearer — and you shouldn’t be calling real APIs in your test environment anyhow.</p>

<p>Keep separate encryption keys for each environment. You could share one, but a leaked development key shouldn’t expose production secrets.</p>

<h2 id="mea-culpa">Mea Culpa</h2>

<p>I originally published this post saying <code class="language-plaintext highlighter-rouge">Rails.app.creds</code> had shipped in Rails 8.1. It hasn’t — it’s on Rails <code class="language-plaintext highlighter-rouge">main</code> and is expected in 8.2. I’ve been running Rails edge on a couple of projects and assumed this had already been released. Apologies for the confusion, and thanks to everyone who pointed it out.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Teach Rails Irregular Plurals with Inflections]]></title>
    <link href="https://andycroll.com/ruby/teach-rails-irregular-plurals-with-inflections/"/>
    <updated>2026-03-30T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/teach-rails-irregular-plurals-with-inflections</id>
    <content type="html"><![CDATA[<p>English has plenty of irregular plurals. Criterion becomes criteria, not criterions. Rails handles many common ones already, but your domain might include words it doesn’t know about.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…accepting Rails’s best guess at a plural:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"criterion"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "criterions"</span>
<span class="s2">"matrix"</span><span class="p">.</span><span class="nf">pluralize</span>     <span class="c1">#=&gt; "matrices"  # this one Rails knows!</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.irregular</code> to teach Rails the correct pair:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"criterion"</span><span class="p">,</span> <span class="s2">"criteria"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"criterion"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "criteria"</span>
<span class="s2">"criteria"</span><span class="p">.</span><span class="nf">singularize</span> <span class="c1">#=&gt; "criterion"</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>Give <code class="language-plaintext highlighter-rouge">irregular</code> the singular and plural forms and Rails handles both directions—<code class="language-plaintext highlighter-rouge">pluralize</code> and <code class="language-plaintext highlighter-rouge">singularize</code> both work correctly.</p>

<p>A <code class="language-plaintext highlighter-rouge">Criterion</code> model will look for a <code class="language-plaintext highlighter-rouge">criteria</code> table. <code class="language-plaintext highlighter-rouge">resources :criteria</code> will route to <code class="language-plaintext highlighter-rouge">CriteriaController</code>. Association names, fixtures, and factory names all follow suit.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Criterion</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="c1"># table: criteria</span>
<span class="k">end</span>

<span class="k">class</span> <span class="nc">Survey</span> <span class="o">&lt;</span> <span class="no">ApplicationRecord</span>
  <span class="n">has_many</span> <span class="ss">:criteria</span>  <span class="c1"># works as expected</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You can declare as many as you need:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"criterion"</span><span class="p">,</span> <span class="s2">"criteria"</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">irregular</span> <span class="s2">"goose"</span><span class="p">,</span> <span class="s2">"geese"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Although, unless you’re building some kind of flighted animal tracker, you probably won’t need that second one.</p>

<p>Rails already knows a handful of irregular plurals: person/people, man/men, child/children, sex/sexes, move/moves, and—crucially—zombie/zombies are built in. Rails’s pluralisation rules are regex-based, so the <code class="language-plaintext highlighter-rouge">(m)an → (m)en</code> pattern also covers woman/women. But that’s it—words like tooth/teeth, foot/feet, mouse/mice, and goose/geese are not handled by default. Check the <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">default inflections</a> to see what’s already covered.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-irregular">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>Before adding an irregular inflection, check whether Rails already knows the word. Try it in a console:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"person"</span><span class="p">.</span><span class="nf">pluralize</span>  <span class="c1">#=&gt; "people"  — already works</span>
<span class="s2">"axis"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "axes"    — already works</span>
</code></pre></div></div>

<p>If it’s already correct, adding it to your initialiser is just noise.</p>

<p>If the word never appears as a model or resource name, there’s no reason to declare it.</p>

<p>For words that don’t change between singular and plural (like “sheep” or “metadata”), you need <code class="language-plaintext highlighter-rouge">inflect.uncountable</code>. For casing issues with acronyms like API or CSV, look at <code class="language-plaintext highlighter-rouge">inflect.acronym</code>.</p>
]]></content>
  </entry>
  
  <entry>
    <title type="html"><![CDATA[Handle Uncountable Words in Rails Inflections]]></title>
    <link href="https://andycroll.com/ruby/handle-uncountable-words-in-rails-inflections/"/>
    <updated>2026-03-23T06:00:00+00:00</updated>
    <id>https://andycroll.com/ruby/handle-uncountable-words-in-rails-inflections</id>
    <content type="html"><![CDATA[<p>Some English words don’t have a separate plural form. “Staff” is staff, “metadata” is metadata, “feedback” is feedback. Rails doesn’t always know this—it will happily generate a <code class="language-plaintext highlighter-rouge">staffs</code> table or a <code class="language-plaintext highlighter-rouge">metadatas</code> route if you let it.</p>

<h2 id="instead-of">Instead of…</h2>

<p>…fighting Rails when it pluralises words that shouldn’t change:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"staff"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "staffs"</span>
<span class="s2">"metadata"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "metadatas"</span>
<span class="s2">"feedback"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "feedbacks"</span>
</code></pre></div></div>

<h2 id="use">Use…</h2>

<p>…<code class="language-plaintext highlighter-rouge">inflect.uncountable</code> to tell Rails these words stay the same:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/initializers/inflections.rb</span>
<span class="no">ActiveSupport</span><span class="o">::</span><span class="no">Inflector</span><span class="p">.</span><span class="nf">inflections</span><span class="p">(</span><span class="ss">:en</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">inflect</span><span class="o">|</span>
  <span class="n">inflect</span><span class="p">.</span><span class="nf">uncountable</span> <span class="sx">%w[staff metadata feedback]</span>
<span class="k">end</span>
</code></pre></div></div>

<p>Now:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"staff"</span><span class="p">.</span><span class="nf">pluralize</span>    <span class="c1">#=&gt; "staff"</span>
<span class="s2">"metadata"</span><span class="p">.</span><span class="nf">pluralize</span> <span class="c1">#=&gt; "metadata"</span>
<span class="s2">"staff"</span><span class="p">.</span><span class="nf">singularize</span>  <span class="c1">#=&gt; "staff"</span>
</code></pre></div></div>

<h2 id="why">Why?</h2>

<p>Table names, route helpers, association names, and autoloading all depend on correct inflection. When Rails gets it wrong, you end up with a <code class="language-plaintext highlighter-rouge">staffs</code> table or <code class="language-plaintext highlighter-rouge">metadatas_path</code> route helpers.</p>

<p>Declaring a word as uncountable fixes this everywhere at once. The <code class="language-plaintext highlighter-rouge">Staff</code> model maps to the <code class="language-plaintext highlighter-rouge">staff</code> table. <code class="language-plaintext highlighter-rouge">resources :staff</code> generates the routes you’d expect.</p>

<p>Words worth declaring uncountable: <code class="language-plaintext highlighter-rouge">staff</code>, <code class="language-plaintext highlighter-rouge">metadata</code>, <code class="language-plaintext highlighter-rouge">feedback</code>, <code class="language-plaintext highlighter-rouge">analytics</code>, <code class="language-plaintext highlighter-rouge">aircraft</code>, <code class="language-plaintext highlighter-rouge">software</code>. You only need to add ones you’re actually using as model or resource names. You can pass a single string or an array.</p>

<p>Rails already handles some common uncountable words—<code class="language-plaintext highlighter-rouge">equipment</code>, <code class="language-plaintext highlighter-rouge">information</code>, <code class="language-plaintext highlighter-rouge">rice</code>, <code class="language-plaintext highlighter-rouge">money</code>, <code class="language-plaintext highlighter-rouge">species</code>, <code class="language-plaintext highlighter-rouge">series</code>, <code class="language-plaintext highlighter-rouge">fish</code>, <code class="language-plaintext highlighter-rouge">sheep</code>, <code class="language-plaintext highlighter-rouge">jeans</code>, and <code class="language-plaintext highlighter-rouge">police</code> work out of the box. Check the <a href="https://github.com/rails/rails/blob/main/activesupport/lib/active_support/inflections.rb">default inflections</a> to see the full list before adding your own.</p>

<p>See the <a href="https://api.rubyonrails.org/classes/ActiveSupport/Inflector/Inflections.html#method-i-uncountable">ActiveSupport::Inflector::Inflections documentation</a> for details.</p>

<h2 id="why-not">Why not?</h2>

<p>Uncountable words make associations slightly less intuitive. <code class="language-plaintext highlighter-rouge">has_many :staff</code> reads naturally, but <code class="language-plaintext highlighter-rouge">Staff.all</code> returning multiple records from a <code class="language-plaintext highlighter-rouge">staff</code> table can briefly confuse developers expecting a <code class="language-plaintext highlighter-rouge">staffs</code> table.</p>

<p>If the word is domain-specific jargon your team invented, a regular plural might actually be clearer. Reserve <code class="language-plaintext highlighter-rouge">uncountable</code> for genuinely uncountable English words, not as a shortcut to avoid a table name you don’t like.</p>

<p>This only affects pluralisation. For casing issues with acronyms like API or CSV, that’s <code class="language-plaintext highlighter-rouge">inflect.acronym</code>. For words with non-standard plurals like criterion/criteria, that’s <code class="language-plaintext highlighter-rouge">inflect.irregular</code>.</p>
]]></content>
  </entry>
  
</feed>
