<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Zil Norvilis</title>
    <description>The latest articles on Forem by Zil Norvilis (@zilton7).</description>
    <link>https://forem.com/zilton7</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F352433%2F2aff65b6-dba1-4f8c-8aaf-0bc84763ae23.jpg</url>
      <title>Forem: Zil Norvilis</title>
      <link>https://forem.com/zilton7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/zilton7"/>
    <language>en</language>
    <item>
      <title>It Looks Like Ruby, But It’s Not: How to Understand Elixir</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Wed, 15 Apr 2026 23:16:57 +0000</pubDate>
      <link>https://forem.com/zilton7/it-looks-like-ruby-but-its-not-how-to-understand-elixir-1db5</link>
      <guid>https://forem.com/zilton7/it-looks-like-ruby-but-its-not-how-to-understand-elixir-1db5</guid>
      <description>&lt;p&gt;If you write Ruby, you will eventually get curious about Elixir. The creator of Elixir, José Valim, was a huge figure in the Ruby on Rails community. Because of this, when you look at Elixir code for the first time, it feels very familiar. It has &lt;code&gt;def&lt;/code&gt;, &lt;code&gt;do&lt;/code&gt;, and &lt;code&gt;end&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;But here is the trap: &lt;strong&gt;Elixir is not Ruby.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;Very often I see Ruby developers try to write Elixir as if it was Ruby. They get frustrated because things don't work the way they expect. Ruby is Object-Oriented. Elixir is Functional. &lt;/p&gt;

&lt;p&gt;To learn Elixir, you don't need to learn a crazy new syntax. You just need to rewire how you think about data. Here is a simple guide translating Ruby concepts into Elixir concepts.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Classes vs Modules
&lt;/h2&gt;

&lt;p&gt;In Ruby, everything is an Object. You create a Class, initialize an object, and that object holds its own data (state).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ruby&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt;
  &lt;span class="nb"&gt;attr_accessor&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;name&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;upcase_name!&lt;/span&gt;
    &lt;span class="vi"&gt;@name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"zil"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase_name!&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="c1"&gt;# Outputs: ZIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Elixir, there are no objects and no classes. Data is just data, and functions are just functions. They live separately. Instead of Classes, you group functions together inside &lt;strong&gt;Modules&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# elixir&lt;/span&gt;
&lt;span class="k"&gt;defmodule&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# We just take data in, and return new data out&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="n"&gt;upcase_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_map&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="no"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_map&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upcase&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"zil"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# We pass the data into the module's function&lt;/span&gt;
&lt;span class="n"&gt;new_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upcase_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;new_user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="c1"&gt;# Outputs: ZIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Method Chaining vs The Pipe Operator
&lt;/h2&gt;

&lt;p&gt;Ruby developers love method chaining. You take an object and call methods on it in a row.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ruby&lt;/span&gt;
&lt;span class="s2"&gt;"hello world"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upcase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Outputs: "HELLO-WORLD"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because Elixir doesn't have objects, you can't call &lt;code&gt;.upcase&lt;/code&gt; on a string. You have to pass the string into a &lt;code&gt;String&lt;/code&gt; module. Doing this normally looks very messy and nested: &lt;code&gt;Enum.join(String.split(String.upcase("hello world"), " "), "-")&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To fix this, Elixir uses the &lt;strong&gt;Pipe Operator (&lt;code&gt;|&amp;gt;&lt;/code&gt;)&lt;/strong&gt;. It takes the result of the left side and passes it as the very first argument to the function on the right side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# elixir&lt;/span&gt;
&lt;span class="s2"&gt;"hello world"&lt;/span&gt;
&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upcase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;|&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;Enum&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"-"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Outputs: "HELLO-WORLD"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you get used to the Pipe Operator, you will actually miss it when you go back to Ruby. It makes reading the flow of data incredibly easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Hashes vs Maps
&lt;/h2&gt;

&lt;p&gt;In Ruby, we use Hashes everywhere to store key-value data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ruby&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Zil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role: &lt;/span&gt;&lt;span class="s2"&gt;"admin"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Elixir, the equivalent is called a &lt;strong&gt;Map&lt;/strong&gt;. The syntax is almost identical, except you put a &lt;code&gt;%&lt;/code&gt; sign in front of it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# elixir&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Zil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role:&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The big difference:&lt;/strong&gt; In Ruby, you can just change the hash later (&lt;code&gt;user[:role] = "user"&lt;/code&gt;). In Elixir, data is &lt;strong&gt;immutable&lt;/strong&gt;. You cannot change the map once it is created. You have to create a &lt;em&gt;brand new map&lt;/em&gt; with the updated value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# elixir&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Zil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role:&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# This creates a completely new map in memory&lt;/span&gt;
&lt;span class="n"&gt;updated_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="ss"&gt;role:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. The Equals Sign is a Lie (Pattern Matching)
&lt;/h2&gt;

&lt;p&gt;This is the hardest part for Rubyists to grasp. &lt;/p&gt;

&lt;p&gt;In Ruby, &lt;code&gt;=&lt;/code&gt; means assignment. You are saying "Put this value into this variable."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ruby&lt;/span&gt;
&lt;span class="nb"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Zil"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Elixir, &lt;code&gt;=&lt;/code&gt; is actually the &lt;strong&gt;Match Operator&lt;/strong&gt;. It is like an algebra equation. It tries to make the left side match the right side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight elixir"&gt;&lt;code&gt;&lt;span class="c1"&gt;# elixir&lt;/span&gt;
&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Zil"&lt;/span&gt; &lt;span class="c1"&gt;# This works, Elixir binds "Zil" to name to make it match.&lt;/span&gt;

&lt;span class="c1"&gt;# But you can also do this:&lt;/span&gt;
&lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;%{&lt;/span&gt;&lt;span class="ss"&gt;name:&lt;/span&gt; &lt;span class="s2"&gt;"Zil"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;role:&lt;/span&gt; &lt;span class="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="no"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;user_name&lt;/span&gt; &lt;span class="c1"&gt;# Outputs: "Zil"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In that second example, Elixir looks at the right side, sees the map, and says: &lt;em&gt;"To make the left side match, I need to extract the value of :name and put it into the &lt;code&gt;user_name&lt;/code&gt; variable."&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;This is called &lt;strong&gt;Pattern Matching&lt;/strong&gt;. It is the most powerful feature in Elixir. You use it everywhere - to extract data from APIs, to handle errors, and to route web requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Learning Elixir when you already know Ruby is a really fun experience. You don't have to fight with ugly brackets or semicolons. &lt;/p&gt;

&lt;p&gt;Just remember the golden rules of Elixir:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Data never changes (Immutability).&lt;/li&gt;
&lt;li&gt;Data and Functions live separately.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;|&amp;gt;&lt;/code&gt; to push data through your functions.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's pretty much it. Even if you never build a production app in Elixir, learning how functional programming works will absolutely make you a better, cleaner Ruby developer.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>elixir</category>
      <category>learning</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Pundit vs CanCanCan vs Action Policy: Which Rails Auth Gem Wins?</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 14 Apr 2026 23:17:20 +0000</pubDate>
      <link>https://forem.com/zilton7/pundit-vs-cancancan-vs-action-policy-which-rails-auth-gem-wins-1ghc</link>
      <guid>https://forem.com/zilton7/pundit-vs-cancancan-vs-action-policy-which-rails-auth-gem-wins-1ghc</guid>
      <description>&lt;p&gt;Sometimes I find myself starting a new Rails project, and almost immediately, I hit a wall: &lt;strong&gt;User Permissions&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Guests can read posts, users can edit their own posts, and admins can delete everything. You need an authorization system to manage this. If you try to write this logic directly inside your controllers or views with a bunch of &lt;code&gt;if/else&lt;/code&gt; statements, your code will become an unreadable mess within a week.&lt;/p&gt;

&lt;p&gt;For a long time, the Rails community was divided. Today, we have three main gems to handle this: &lt;strong&gt;CanCanCan&lt;/strong&gt;, &lt;strong&gt;Pundit&lt;/strong&gt;, and the newer &lt;strong&gt;Action Policy&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Here is my honest breakdown of how they work, the pros and cons of each, and which one you should actually use for your next app.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. CanCanCan (The Legacy Giant)
&lt;/h2&gt;

&lt;p&gt;If you learned Rails 5 or 6 years ago, you probably used CanCanCan (a community continuation of the original &lt;code&gt;cancan&lt;/code&gt; gem). &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
It is highly centralized. You define absolutely every permission for your entire application inside one single file called the &lt;code&gt;Ability&lt;/code&gt; class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/ability.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Ability&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;CanCan&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Ability&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="c1"&gt;# guest user&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt;
      &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="ss"&gt;:manage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:all&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="ss"&gt;:read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;
      &lt;span class="n"&gt;can&lt;/span&gt; &lt;span class="ss"&gt;:update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, in your controller, you just call &lt;code&gt;authorize! :update, @post&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt; For a very small app, it is incredibly fast to set up. You can see all the rules in one place.&lt;br&gt;
&lt;strong&gt;The Bad:&lt;/strong&gt; As your app grows, the &lt;code&gt;Ability&lt;/code&gt; file becomes a nightmare. I have seen production apps with an &lt;code&gt;ability.rb&lt;/code&gt; file that is 2,000 lines long. It becomes impossible to test and incredibly slow to load, because Rails has to evaluate that entire massive file on every single request.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Pundit (The Object-Oriented Standard)
&lt;/h2&gt;

&lt;p&gt;Pundit was created to solve the "giant file" problem of CanCanCan. It threw away the custom syntax and went back to plain, simple Ruby.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
Instead of one big file, you create a separate "Policy" class for every single model in your app.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/policies/post_policy.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostPolicy&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:post&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
    &lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update?&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your controller, you use the same &lt;code&gt;authorize @post&lt;/code&gt; method, and Pundit automatically looks for the &lt;code&gt;PostPolicy&lt;/code&gt; and calls the &lt;code&gt;update?&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt; It is incredibly clean. Because it is just plain Ruby objects returning &lt;code&gt;true&lt;/code&gt; or &lt;code&gt;false&lt;/code&gt;, writing tests for your policies is super easy. It scales perfectly with huge applications.&lt;br&gt;
&lt;strong&gt;The Bad:&lt;/strong&gt; It can feel a bit repetitive. You end up writing a lot of boilerplate &lt;code&gt;initialize&lt;/code&gt; methods for every single policy file.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Action Policy (The Modern Speed Demon)
&lt;/h2&gt;

&lt;p&gt;Action Policy is the newest challenger, built by the team at Evil Martians. It looked at Pundit and said: &lt;em&gt;"This is great, but we can make it faster and require less typing."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
It looks very similar to Pundit, but it gives you a base class to inherit from, which removes the need to write &lt;code&gt;initialize&lt;/code&gt; methods. It also adds powerful features right out of the box.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/policies/post_policy.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostPolicy&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationPolicy&lt;/span&gt;
  &lt;span class="c1"&gt;# We can alias methods so we don't repeat code!&lt;/span&gt;
  &lt;span class="n"&gt;alias_rule&lt;/span&gt; &lt;span class="ss"&gt;:edit?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:destroy?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: :update?&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;update?&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;admin?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user_id&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt; &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Performance:&lt;/strong&gt; It is heavily optimized. It caches authorization results during a request. If you check if a user can update a post 50 times in a view loop, Action Policy only calculates it once. Pundit calculates it 50 times.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Less Code:&lt;/strong&gt; Features like &lt;code&gt;alias_rule&lt;/code&gt; save you from writing duplicate methods.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;GraphQL Integration:&lt;/strong&gt; If you are building a modern API with Ruby-GraphQL, Action Policy integrates flawlessly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Bad:&lt;/strong&gt; It has a slightly larger learning curve if you want to use its advanced caching and scoping features compared to the absolute simplicity of Pundit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Which one should you pick?
&lt;/h2&gt;

&lt;p&gt;Don't overcomplicate your decision. Here is the golden rule I follow today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Do not use CanCanCan for new projects.&lt;/strong&gt; It is great for legacy codebases, but the centralized design pattern is an anti-pattern for modern, scalable web apps.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use Pundit&lt;/strong&gt; if you want the most "standard" approach. Almost every Rails developer knows how to read Pundit code, and the documentation is everywhere.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use Action Policy&lt;/strong&gt; if you are building a highly performant app, an API, or if you just hate writing boilerplate code. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Personally, I have completely switched to &lt;strong&gt;Action Policy&lt;/strong&gt; for all my new Rails 8 apps. The built-in caching and the cleaner syntax make it the absolute winner for modern Ruby development.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>security</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Wise Testing: What to Test (and Ignore) as a Solo Rails Developer</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Mon, 13 Apr 2026 23:22:57 +0000</pubDate>
      <link>https://forem.com/zilton7/wise-testing-what-to-test-and-ignore-as-a-solo-rails-developer-515h</link>
      <guid>https://forem.com/zilton7/wise-testing-what-to-test-and-ignore-as-a-solo-rails-developer-515h</guid>
      <description>&lt;p&gt;Solo founders fail for a completely preventable reason. It’s not because their idea was bad, and it’s not because they couldn't code. It’s because they spent 4 weeks writing unit tests for a product that had zero paying customers.&lt;/p&gt;

&lt;p&gt;In the enterprise world, 100% test coverage is an insurance policy. In the startup world, as a One-Person Team, &lt;strong&gt;100% test coverage is a death sentence.&lt;/strong&gt; You will run out of momentum and quit before you launch.&lt;/p&gt;

&lt;p&gt;But you can't just ship with &lt;em&gt;zero&lt;/em&gt; tests, or you will be too terrified to deploy updates on a Friday. &lt;/p&gt;

&lt;p&gt;You need &lt;strong&gt;Wise Testing&lt;/strong&gt;. This is the art of getting 90% confidence with 10% of the code. By sticking to Rails defaults (Minitest and Fixtures), you can build a safety net that protects your business without slowing down your MVP. &lt;/p&gt;

&lt;p&gt;Here is my exact guide on what to test, and more importantly, what to completely ignore.&lt;/p&gt;

&lt;h2&gt;
  
  
  RULE 1: What NOT to Test (The Time Wasters)
&lt;/h2&gt;

&lt;p&gt;The biggest mistake beginners make is testing the Rails framework. Rails has thousands of contributors who already tested &lt;code&gt;ActiveRecord&lt;/code&gt;. You do not need to test it again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do NOT test validations:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# A useless test&lt;/span&gt;
&lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"user is invalid without an email"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="kp"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;assert_not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid?&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you wrote &lt;code&gt;validates :email, presence: true&lt;/code&gt; in your model, trust that Rails works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do NOT test basic CRUD controllers:&lt;/strong&gt;&lt;br&gt;
Don't write isolated controller tests just to check if the &lt;code&gt;index&lt;/code&gt; action returns a 200 status code. It is a massive waste of time. Your System Tests will catch if the page crashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do NOT test third-party UI:&lt;/strong&gt;&lt;br&gt;
If you are using Tailwind UI or a component library, don't write tests to ensure a button is blue. &lt;/p&gt;
&lt;h2&gt;
  
  
  RULE 2: The "Golden Path" System Tests (High ROI)
&lt;/h2&gt;

&lt;p&gt;If you only have time to write one type of test, write &lt;strong&gt;System Tests&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;A system test boots up a real browser (using Capybara and Selenium/Playwright), navigates your app, and clicks buttons like a real human. &lt;/p&gt;

&lt;p&gt;Why is this the best use of your time? Because one system test implicitly tests your routing, your controller, your database, and your view rendering all at once.&lt;/p&gt;

&lt;p&gt;Identify the &lt;strong&gt;"Golden Paths"&lt;/strong&gt; of your app. These are the 2 or 3 flows that &lt;em&gt;must&lt;/em&gt; work for your business to survive. &lt;/p&gt;

&lt;p&gt;For a SaaS, the Golden Path is usually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User signs up.&lt;/li&gt;
&lt;li&gt;User creates the core resource (e.g., a "Project").&lt;/li&gt;
&lt;li&gt;User upgrades to a paid plan.
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test/system/onboarding_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"application_system_test_case"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnboardingTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationSystemTestCase&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"user can sign up and create a project"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# 1. Sign up&lt;/span&gt;
    &lt;span class="n"&gt;visit&lt;/span&gt; &lt;span class="n"&gt;new_user_registration_path&lt;/span&gt;
    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"founder@example.com"&lt;/span&gt;
    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"secret123"&lt;/span&gt;
    &lt;span class="n"&gt;click_on&lt;/span&gt; &lt;span class="s2"&gt;"Sign Up"&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Core Business Action&lt;/span&gt;
    &lt;span class="n"&gt;click_on&lt;/span&gt; &lt;span class="s2"&gt;"New Project"&lt;/span&gt;
    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"My Awesome MVP"&lt;/span&gt;
    &lt;span class="n"&gt;click_on&lt;/span&gt; &lt;span class="s2"&gt;"Save"&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Verify Success&lt;/span&gt;
    &lt;span class="n"&gt;assert_text&lt;/span&gt; &lt;span class="s2"&gt;"Project was successfully created."&lt;/span&gt;
    &lt;span class="n"&gt;assert_text&lt;/span&gt; &lt;span class="s2"&gt;"My Awesome MVP"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If this single test passes, you have a 90% guarantee that your app is functional. &lt;/p&gt;
&lt;h2&gt;
  
  
  RULE 3: Surgical Unit Tests (The Money and the Math)
&lt;/h2&gt;

&lt;p&gt;You skipped testing basic models and controllers. So when &lt;em&gt;do&lt;/em&gt; you write unit tests?&lt;/p&gt;

&lt;p&gt;You write them for &lt;strong&gt;Custom Business Logic&lt;/strong&gt;. If a method calculates a tax rate, processes a Stripe webhook, or filters sensitive data, you must test it in isolation. &lt;/p&gt;

&lt;p&gt;Always extract this complex logic into Plain Old Ruby Objects (Service Objects), and test those.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test/services/commission_calculator_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"test_helper"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CommissionCalculatorTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;TestCase&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"calculates 10 percent commission for standard affiliates"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# Standard math logic that MUST be right&lt;/span&gt;
    &lt;span class="n"&gt;calculator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CommissionCalculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="mi"&gt;100_00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;rate: &lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="mi"&gt;10_00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payout&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"returns zero if order is refunded"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;calculator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CommissionCalculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="mi"&gt;100_00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;rate: &lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;refunded: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;assert_equal&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;calculator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payout&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test the things that would cause you to lose money or leak private data if they broke. &lt;/p&gt;

&lt;h2&gt;
  
  
  RULE 4: Embrace Rails Fixtures (Skip FactoryBot)
&lt;/h2&gt;

&lt;p&gt;The RSpec world loves &lt;code&gt;FactoryBot&lt;/code&gt;. Factories are great, but they dynamically generate and insert database records on every single test run. As your app grows, this makes your test suite agonizingly slow. &lt;/p&gt;

&lt;p&gt;Rails defaults to &lt;strong&gt;Fixtures&lt;/strong&gt;. Fixtures are simply YAML files that get loaded into your test database &lt;em&gt;once&lt;/em&gt; when the test suite boots.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test/fixtures/users.yml&lt;/span&gt;
&lt;span class="na"&gt;zil&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;zil@example.com&lt;/span&gt;
  &lt;span class="na"&gt;encrypted_password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;%= User.new.send(:password_digest, 'password') %&amp;gt;&lt;/span&gt;
  &lt;span class="na"&gt;plan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Inside your tests, you just reference the name:&lt;/span&gt;
&lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:zil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a solo developer, Fixtures are the ultimate speed hack. Your Minitest suite will run in a fraction of a second, meaning you will actually run it frequently while coding.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Want the complete system?&lt;/strong&gt;&lt;br&gt;
If this pragmatic approach resonates with you and you want to see exactly how I set up my test suites from scratch, I’ve put my entire testing workflow into a comprehensive guide. Check out &lt;strong&gt;&lt;a href="https://norvilis.gumroad.com/l/wise-testing?utc_source=devtopost" rel="noopener noreferrer"&gt;Wise Testing: The Solo Founder's Guide to Rails Quality&lt;/a&gt;&lt;/strong&gt; to learn how to test Stripe webhooks, handle complex fixtures, and set up lightning-fast CI pipelines.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Summary: The Wise Testing Checklist
&lt;/h2&gt;

&lt;p&gt;Before you write a test, ask yourself: &lt;em&gt;"If this breaks, does the business fail, or does it just look a little weird?"&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Do not test Rails.&lt;/strong&gt; (Validations, associations, simple controllers).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Write 3-5 System Tests.&lt;/strong&gt; Cover the absolute critical paths (Signup, Core Value, Checkout).&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Write Surgical Unit Tests.&lt;/strong&gt; Only test complex math, money, and security logic.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use Minitest and Fixtures.&lt;/strong&gt; Keep your test suite boring, fast, and dependency-free.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ship the MVP. Let the users find the small bugs. Use your automated tests to protect the big ones.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>testing</category>
      <category>startup</category>
    </item>
    <item>
      <title>Stop AI Spaghetti: Enforcing Rails Architecture in 2026</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sun, 12 Apr 2026 23:12:24 +0000</pubDate>
      <link>https://forem.com/zilton7/stop-ai-spaghetti-enforcing-rails-architecture-in-2026-2fob</link>
      <guid>https://forem.com/zilton7/stop-ai-spaghetti-enforcing-rails-architecture-in-2026-2fob</guid>
      <description>&lt;p&gt;We are fully in the era of autonomous coding. If you use tools like Cursor, Windsurf, or Copilot Workspace, you know how fast you can move. You type a prompt, and the AI generates an entire feature - controllers, models, and views—in 10 seconds.&lt;/p&gt;

&lt;p&gt;But there is a dark side to this speed. &lt;/p&gt;

&lt;p&gt;The AI does not know your personal architectural standards. If you don't watch it closely, it will put complex business logic directly into your ActiveRecord callbacks. It will make raw API calls from your ERB views. Give it a few months, and your beautiful Rails app will turn into an unmaintainable "Big Ball of Mud."&lt;/p&gt;

&lt;p&gt;When you are generating code this fast, you cannot rely on willpower to keep things clean. You need automated guardrails. Here is how to ensure autonomously generated code stays consistent with your Rails architecture over time.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Context File (&lt;code&gt;.cursorrules&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;The easiest way to fix bad AI code is to prevent it before it is written. &lt;/p&gt;

&lt;p&gt;Modern AI code editors look for hidden context files in your project root (like &lt;code&gt;.cursorrules&lt;/code&gt; or &lt;code&gt;.windsurfrules&lt;/code&gt;). This file tells the AI &lt;em&gt;how&lt;/em&gt; you expect it to behave in this specific repository. &lt;/p&gt;

&lt;p&gt;Instead of typing "Use a service object" in every single prompt, you define your architectural boundaries once.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.cursorrules&lt;/code&gt; file in your Rails root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# Rails Architecture Guidelines for this Project&lt;/span&gt;
&lt;span class="p"&gt;
1.&lt;/span&gt; &lt;span class="gs"&gt;**Fat Models, Skinny Controllers are BANNED.**&lt;/span&gt; Controllers should only handle HTTP routing and params. Models should only handle database associations and strict validations.
&lt;span class="p"&gt;2.&lt;/span&gt; &lt;span class="gs"&gt;**Business Logic:**&lt;/span&gt; ALL business logic must go into Plain Old Ruby Objects (POROs) inside &lt;span class="sb"&gt;`app/services/`&lt;/span&gt;. Do not use ActiveRecord callbacks (&lt;span class="sb"&gt;`after_save`&lt;/span&gt;, &lt;span class="sb"&gt;`after_create`&lt;/span&gt;) for business logic like sending emails or calling external APIs.
&lt;span class="p"&gt;3.&lt;/span&gt; &lt;span class="gs"&gt;**Views:**&lt;/span&gt; Use Tailwind CSS for styling. Extract complex UI elements into &lt;span class="sb"&gt;`ViewComponent`&lt;/span&gt; classes instead of using Rails &lt;span class="sb"&gt;`_partials`&lt;/span&gt;.
&lt;span class="p"&gt;4.&lt;/span&gt; &lt;span class="gs"&gt;**Testing:**&lt;/span&gt; Write Minitest System Specs for all new features. Do not write granular unit tests for private methods.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you tell the AI to "Build a user onboarding flow," it automatically knows to generate a &lt;code&gt;UserOnboardingService&lt;/code&gt; instead of dumping 200 lines of code into the &lt;code&gt;UsersController&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: Ruthless Linting with RuboCop
&lt;/h2&gt;

&lt;p&gt;AI is notoriously bad at code formatting. It will mix single and double quotes, mess up indentation, and write methods that are 50 lines long. &lt;/p&gt;

&lt;p&gt;You need to enforce style programmatically. In Ruby, this means &lt;strong&gt;RuboCop&lt;/strong&gt;. But you need to turn on the strict Rails-specific rules.&lt;/p&gt;

&lt;p&gt;Add these to your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ss"&gt;:development&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:test&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rubocop'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rubocop-rails'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rubocop-performance'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;require: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;.rubocop.yml&lt;/code&gt; file and set your boundaries. For example, if you want to stop the AI from writing massive, complex methods, enforce the &lt;code&gt;MethodLength&lt;/code&gt; and &lt;code&gt;Metrics/AbcSize&lt;/code&gt; cops.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .rubocop.yml&lt;/span&gt;
&lt;span class="na"&gt;require&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-rails&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;rubocop-performance&lt;/span&gt;

&lt;span class="na"&gt;Metrics/MethodLength&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;15&lt;/span&gt;

&lt;span class="na"&gt;Metrics/ClassLength&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Max&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;

&lt;span class="na"&gt;Rails/SkipsModelValidations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;Enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# Stops AI from using .update_attribute and skipping your validations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Pro Move:&lt;/strong&gt; Set your editor to "Auto-Fix on Save." When the AI generates a messy file, you just hit &lt;code&gt;Cmd+S&lt;/code&gt;, and RuboCop instantly rewrites the syntax to match your standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Enforcing Boundaries (Packwerk)
&lt;/h2&gt;

&lt;p&gt;If your app is growing large, the AI will eventually try to cross-wire different domains. It will make the &lt;code&gt;Billing&lt;/code&gt; module call private methods inside the &lt;code&gt;Inventory&lt;/code&gt; module. &lt;/p&gt;

&lt;p&gt;To stop this structurally, you can use Shopify's &lt;strong&gt;Packwerk&lt;/strong&gt; gem. Packwerk allows you to define strict boundaries between folders in your Rails app.&lt;/p&gt;

&lt;p&gt;You create a &lt;code&gt;package.yml&lt;/code&gt; file in your folders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/services/billing/package.yml&lt;/span&gt;
&lt;span class="na"&gt;enforce_privacy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;enforce_dependencies&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the AI generates code in the &lt;code&gt;Orders&lt;/code&gt; controller that tries to call an internal class inside the &lt;code&gt;Billing&lt;/code&gt; package, Packwerk will throw a static analysis error before you even run the code. It literally blocks the AI from creating spaghetti dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: System Tests over Unit Tests
&lt;/h2&gt;

&lt;p&gt;When you use AI, your internal code changes very fast. You might ask the AI to refactor a Service Object, and it will completely rename all the internal methods. &lt;/p&gt;

&lt;p&gt;If you have 100 granular Unit Tests checking those specific method names, your test suite will break every time you prompt the AI. You will spend hours fixing tests instead of shipping features.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To survive the AI era, you must rely on System Tests (Integration Tests).&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test/system/onboarding_test.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s2"&gt;"application_system_test_case"&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OnboardingTest&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationSystemTestCase&lt;/span&gt;
  &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="s2"&gt;"user can sign up and reach the dashboard"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;visit&lt;/span&gt; &lt;span class="n"&gt;new_user_registration_path&lt;/span&gt;
    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Email"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"test@example.com"&lt;/span&gt;
    &lt;span class="n"&gt;fill_in&lt;/span&gt; &lt;span class="s2"&gt;"Password"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: &lt;/span&gt;&lt;span class="s2"&gt;"password123"&lt;/span&gt;
    &lt;span class="n"&gt;click_on&lt;/span&gt; &lt;span class="s2"&gt;"Sign up"&lt;/span&gt;

    &lt;span class="n"&gt;assert_text&lt;/span&gt; &lt;span class="s2"&gt;"Welcome to your Dashboard"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;System tests don't care &lt;em&gt;how&lt;/em&gt; the AI wrote the backend logic. They don't care if the AI used a Service Object or a background job. They only care that the user can click the button and get the result. &lt;/p&gt;

&lt;p&gt;By writing System tests, you give the AI the freedom to refactor and optimize the internal architecture without breaking your test suite, while giving yourself 100% confidence that the app still works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In 2026, your job is no longer just typing code. Your job is acting as the &lt;strong&gt;Editor-in-Chief&lt;/strong&gt; for an incredibly fast, slightly reckless junior developer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Give Context:&lt;/strong&gt; Use &lt;code&gt;.cursorrules&lt;/code&gt; to define your architecture up front.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auto-Format:&lt;/strong&gt; Use RuboCop to enforce syntax rules on save.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set Boundaries:&lt;/strong&gt; Use static analysis (like Packwerk) to prevent tangled dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the Output:&lt;/strong&gt; Rely on System Tests to verify behavior, not internal implementation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you set up these guardrails on day one, you can let the AI run wild, knowing your Rails app will stay clean, modular, and easy to maintain.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ai</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>How to Fix N+1 Queries in Rails Like a Pro</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sat, 11 Apr 2026 23:11:25 +0000</pubDate>
      <link>https://forem.com/zilton7/how-to-fix-n1-queries-in-rails-like-a-pro-59b7</link>
      <guid>https://forem.com/zilton7/how-to-fix-n1-queries-in-rails-like-a-pro-59b7</guid>
      <description>&lt;p&gt;There are Rails applications that run incredibly fast on a developer's laptop, but the moment they are deployed to production, they crawl to a halt. The pages take 3 seconds to load, and the database CPU is at 100%.&lt;/p&gt;

&lt;p&gt;Almost every single time, the culprit is the exact same thing: &lt;strong&gt;The N+1 Query Problem.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;ActiveRecord is amazing because it hides the complex SQL from you. But because it is so easy to use, it is also very easy to accidentally hammer your database with hundreds of unnecessary queries. &lt;/p&gt;

&lt;p&gt;Here is exactly what the N+1 problem is, how to fix it using &lt;code&gt;.includes&lt;/code&gt;, and how to handle complex nested data like a senior Rails developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem: What is N+1?
&lt;/h2&gt;

&lt;p&gt;Imagine you have a &lt;code&gt;Post&lt;/code&gt; model and a &lt;code&gt;User&lt;/code&gt; model (the author). You want to list 50 posts on your homepage and show the author's name next to each one.&lt;/p&gt;

&lt;p&gt;You write this in your controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/posts_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And you write this in your view:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/posts/index.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="vi"&gt;@posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;By: &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This looks perfectly fine. But if you look at your terminal logs, you will see a nightmare. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rails runs &lt;strong&gt;1 query&lt;/strong&gt; to fetch the 50 posts.&lt;/li&gt;
&lt;li&gt;Then, as it loops through the HTML, it hits &lt;code&gt;post.user.name&lt;/code&gt;. It doesn't have the user data in memory, so it asks the database: &lt;em&gt;"Hey, get me the user for post 1."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;Then it asks: &lt;em&gt;"Get me the user for post 2."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;It does this 50 times.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You just ran &lt;strong&gt;51 queries&lt;/strong&gt; to load a single webpage. If you have 1,000 posts, you run 1,001 queries. This is the N+1 problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  LEVEL 1: The Basic Fix (&lt;code&gt;includes&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;To fix this, we need to tell ActiveRecord to fetch all the related data &lt;em&gt;before&lt;/em&gt; we start looping in the view. We do this using &lt;strong&gt;Eager Loading&lt;/strong&gt; via the &lt;code&gt;.includes&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Change your controller to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/posts_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="c1"&gt;# We tell Rails: "Fetch the posts, AND fetch their users right now."&lt;/span&gt;
  &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, look at your server logs. Rails will only run &lt;strong&gt;2 queries&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;code&gt;SELECT * FROM posts LIMIT 50&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SELECT * FROM users WHERE id IN (1, 2, 3, 4...)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Rails grabs all 50 users in one big query, and stitches them together in memory. The speed difference is massive.&lt;/p&gt;

&lt;h2&gt;
  
  
  LEVEL 2: Nested Includes (Going Deeper)
&lt;/h2&gt;

&lt;p&gt;What if your data is more complex? &lt;br&gt;
Let's say you want to show the Post, the User's name, the User's Profile picture, AND a list of all the Comments on the post.&lt;/p&gt;

&lt;p&gt;If you just do &lt;code&gt;.includes(:user)&lt;/code&gt;, the comments and the profile will still trigger N+1 queries. You have to pass a &lt;strong&gt;Hash&lt;/strong&gt; to &lt;code&gt;.includes&lt;/code&gt; to load nested relationships.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
  &lt;span class="vi"&gt;@posts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="ss"&gt;comments: :author&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# Loads comments, AND the author of each comment&lt;/span&gt;
    &lt;span class="ss"&gt;user: :profile&lt;/span&gt;        &lt;span class="c1"&gt;# Loads the post user, AND the user's profile&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using arrays and hashes, you can build a perfectly optimized data tree with a single line of code.&lt;/p&gt;

&lt;h2&gt;
  
  
  LEVEL 3: includes vs preload vs eager_load (The Pro Stuff)
&lt;/h2&gt;

&lt;p&gt;When you use &lt;code&gt;.includes&lt;/code&gt;, Rails does something very clever. It looks at your query and decides the best way to fetch the data. But as you get more advanced, you should know exactly what is happening under the hood. &lt;/p&gt;

&lt;p&gt;ActiveRecord actually has three different methods for eager loading:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;.preload&lt;/code&gt; (The Default)&lt;/strong&gt;&lt;br&gt;
This is what &lt;code&gt;.includes&lt;/code&gt; usually does behind the scenes. It ALWAYS runs two separate queries. &lt;br&gt;
&lt;code&gt;SELECT * FROM posts&lt;/code&gt; and then &lt;code&gt;SELECT * FROM users WHERE id IN (...)&lt;/code&gt;. &lt;br&gt;
It is very fast and uses less memory. But, you &lt;strong&gt;cannot&lt;/strong&gt; use a &lt;code&gt;where&lt;/code&gt; clause on the preloaded table. If you try &lt;code&gt;Post.preload(:user).where(users: { active: true })&lt;/code&gt;, your app will crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;.eager_load&lt;/code&gt; (The Giant Join)&lt;/strong&gt;&lt;br&gt;
This forces Rails to use a &lt;code&gt;LEFT OUTER JOIN&lt;/code&gt;. It grabs all the posts and all the users in &lt;strong&gt;one single, massive query&lt;/strong&gt;. &lt;br&gt;
You use this when you specifically need to filter by the associated table:&lt;br&gt;
&lt;code&gt;Post.eager_load(:user).where(users: { active: true })&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. &lt;code&gt;.includes&lt;/code&gt; (The Smart Manager)&lt;/strong&gt;&lt;br&gt;
&lt;code&gt;.includes&lt;/code&gt; is the safe middle ground. By default, it acts like &lt;code&gt;.preload&lt;/code&gt;. But if Rails sees that you added a &lt;code&gt;.where&lt;/code&gt; referencing the joined table, it automatically switches to acting like &lt;code&gt;.eager_load&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rule of thumb:&lt;/em&gt; Just stick to &lt;code&gt;.includes&lt;/code&gt; 90% of the time, and let Rails do the thinking.&lt;/p&gt;
&lt;h2&gt;
  
  
  LEVEL 4: Strict Loading (The Ultimate Safety Net)
&lt;/h2&gt;

&lt;p&gt;Even senior developers accidentally introduce N+1 queries. You might add a new helper method in a view months later and forget to update the controller's &lt;code&gt;.includes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To prevent this from ever reaching production, modern Rails has a feature called &lt;strong&gt;Strict Loading&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you turn this on, Rails will literally crash your app (raise an error) the moment it detects an N+1 query. It forces you to fix it immediately.&lt;/p&gt;

&lt;p&gt;You can do this on a single record:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_loading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;
&lt;span class="vi"&gt;@post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;user&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; Raises ActiveRecord::StrictLoadingViolationError!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, you can do what I do and turn it on globally for your &lt;code&gt;development&lt;/code&gt; and &lt;code&gt;test&lt;/code&gt; environments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/environments/development.rb&lt;/span&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active_record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_loading_by_default&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, you can never accidentally write a view that triggers an N+1 query without your local server screaming at you to add &lt;code&gt;.includes&lt;/code&gt;. It is the best performance habit you can build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;ActiveRecord makes database interactions feel like magic, but you always have to pay attention to the logs. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check your terminal. If you see the same &lt;code&gt;SELECT&lt;/code&gt; statement repeating 50 times in a row, you have an N+1 problem.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;.includes&lt;/code&gt; in your controller to fetch the data upfront.&lt;/li&gt;
&lt;li&gt;Use Hashes for deeply nested associations.&lt;/li&gt;
&lt;li&gt;Turn on &lt;code&gt;strict_loading&lt;/code&gt; in development to catch the bugs before your users do.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's pretty much it. Fixing N+1 queries is usually the easiest way to make a slow Rails app feel 10x faster.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>activerecord</category>
      <category>performance</category>
    </item>
    <item>
      <title>Building a World-Class Search Engine in Rails with Searchkick</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Fri, 10 Apr 2026 23:03:11 +0000</pubDate>
      <link>https://forem.com/zilton7/building-a-world-class-search-engine-in-rails-with-searchkick-3bgf</link>
      <guid>https://forem.com/zilton7/building-a-world-class-search-engine-in-rails-with-searchkick-3bgf</guid>
      <description>&lt;h1&gt;
  
  
  Stop Using SQL LIKE: A Step-by-Step Guide to Elasticsearch in Rails
&lt;/h1&gt;

&lt;p&gt;When you build a standard Rails app, searching your database usually starts with a simple ActiveRecord query: &lt;code&gt;Product.where("name ILIKE ?", "%#{params[:q]}%")&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;This works fine when you have 100 products. But when you have 100,000 products, it gets very slow. Even worse, if a user searches for "iphne" instead of "iphone", your database returns zero results. Users expect Google-level search with auto-complete and typo forgiveness. Postgres can do basic Full Text Search, but setting it up perfectly is painful.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Elasticsearch&lt;/strong&gt; comes in. &lt;/p&gt;

&lt;p&gt;Elasticsearch is essentially a secondary, NoSQL database completely optimized for searching text. Integrating it with Rails sounds intimidating, but thanks to an amazing gem called &lt;strong&gt;Searchkick&lt;/strong&gt;, you can build enterprise-grade search in about 10 minutes.&lt;/p&gt;

&lt;p&gt;Here is the step-by-step guide to adding Elasticsearch to your Rails app without losing your mind.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Setup
&lt;/h2&gt;

&lt;p&gt;First off, you need Elasticsearch running on your computer. The absolute easiest way to do this without messing up your Mac or Linux machine is to use Docker. Run this in your terminal to start a local server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 9200:9200 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"discovery.type=single-node"&lt;/span&gt; docker.elastic.co/elasticsearch/elasticsearch:8.13.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, let's add the gems to your Rails app. We need the official client and the Searchkick wrapper.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Gemfile&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'elasticsearch'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'searchkick'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: Tell Your Model to Listen
&lt;/h2&gt;

&lt;p&gt;Elasticsearch does not magically read your Postgres database. It is a separate engine. We need to tell our Rails model to sync its data to Elasticsearch.&lt;/p&gt;

&lt;p&gt;Open your model (let's use a &lt;code&gt;Product&lt;/code&gt; model) and add one word:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/product.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;searchkick&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. From now on, whenever you &lt;code&gt;create&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, or &lt;code&gt;destroy&lt;/code&gt; a Product, Searchkick will automatically send a background request to Elasticsearch to keep the search index perfectly in sync with your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Index Your Existing Data
&lt;/h2&gt;

&lt;p&gt;Because you just added the gem, Elasticsearch is currently empty. It doesn't know about the products you created yesterday. &lt;/p&gt;

&lt;p&gt;You need to push your existing database records into Elasticsearch. Open your Rails console (&lt;code&gt;rails c&lt;/code&gt;) and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reindex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will see a progress bar as Searchkick grabs all your products and sends them to the search engine. Anytime you make massive database changes (like a raw SQL bulk update), you should run this command.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: The Search Query
&lt;/h2&gt;

&lt;p&gt;Now for the fun part. Let's replace that ugly &lt;code&gt;ILIKE&lt;/code&gt; query in our controller. &lt;/p&gt;

&lt;p&gt;Searchkick gives us a &lt;code&gt;.search&lt;/code&gt; method that feels just like ActiveRecord, but it queries Elasticsearch instead of Postgres.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/products_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProductsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:q&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;presence&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;

    &lt;span class="vi"&gt;@products&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="ss"&gt;fields: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;match: :word_start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;misspellings: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;edit_distance: &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look at how powerful this is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;fields:&lt;/code&gt; We tell it to only look at the name and description.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;match: :word_start&lt;/code&gt; allows for auto-complete. If they type "lap", it matches "laptop".&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;misspellings:&lt;/code&gt; This is the magic. If they type "lpatop", Elasticsearch knows they meant "laptop" because the "edit distance" (number of wrong letters) is within our limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  STEP 5: Customizing the Index (The Pro Move)
&lt;/h2&gt;

&lt;p&gt;By default, Searchkick sends every single column of your database to Elasticsearch. If you have a &lt;code&gt;secret_cost&lt;/code&gt; column or a &lt;code&gt;user_password&lt;/code&gt; column, you absolutely do not want that in your search index. &lt;/p&gt;

&lt;p&gt;You should always control exactly what data gets indexed. You do this by overriding the &lt;code&gt;search_data&lt;/code&gt; method in your model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/product.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Product&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;searchkick&lt;/span&gt;

  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:category&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;search_data&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="c1"&gt;# We can even index associated data!&lt;/span&gt;
      &lt;span class="ss"&gt;category_name: &lt;/span&gt;&lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;in_stock: &lt;/span&gt;&lt;span class="n"&gt;stock_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you run &lt;code&gt;Product.reindex&lt;/code&gt;, only this specific JSON block is sent to Elasticsearch. Because we included &lt;code&gt;category.name&lt;/code&gt;, users can now type "Electronics" in the search bar, and it will return the products belonging to that category, without doing any complex SQL &lt;code&gt;JOIN&lt;/code&gt; queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;That's pretty much it. Adding Elasticsearch used to require hundreds of lines of configuration and complex JSON mapping files. &lt;/p&gt;

&lt;p&gt;With &lt;code&gt;Searchkick&lt;/code&gt;, the workflow is incredibly simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the gem.&lt;/li&gt;
&lt;li&gt;Add &lt;code&gt;searchkick&lt;/code&gt; to your model.&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;Model.reindex&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;Model.search("query")&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your app relies heavily on user discovery - like an e-commerce store, a directory, or a massive blog—ditching SQL for a dedicated search engine is the biggest UX upgrade you can make.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>elasticsearch</category>
      <category>ruby</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Easiest Way to Add Drag and Drop to Your Rails App</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Thu, 09 Apr 2026 23:13:05 +0000</pubDate>
      <link>https://forem.com/zilton7/the-easiest-way-to-add-drag-and-drop-to-your-rails-app-341b</link>
      <guid>https://forem.com/zilton7/the-easiest-way-to-add-drag-and-drop-to-your-rails-app-341b</guid>
      <description>&lt;h1&gt;
  
  
  Building Drag and Drop in Rails 8 with SortableJS and Importmaps
&lt;/h1&gt;

&lt;p&gt;Very often I find myself building apps where users need to reorder things. Maybe it is a list of tasks, a gallery of images, or steps in a project. &lt;/p&gt;

&lt;p&gt;In the old days, we used &lt;code&gt;jQuery UI&lt;/code&gt; for this. Then we moved to complex React drag-and-drop libraries. But if you are using modern Rails with Hotwire, adding drag and drop is actually incredibly simple. We don't even need Node.js or Webpack. &lt;/p&gt;

&lt;p&gt;We can use a lightweight library called &lt;strong&gt;SortableJS&lt;/strong&gt;, load it via &lt;strong&gt;Importmaps&lt;/strong&gt;, and connect it to our database using a single Stimulus controller. &lt;/p&gt;

&lt;p&gt;Here is exactly how to do it in 5 steps.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Database Setup
&lt;/h2&gt;

&lt;p&gt;First off, our database needs to know the order of our items. Let's assume we have a &lt;code&gt;Task&lt;/code&gt; model. We need to add a &lt;code&gt;position&lt;/code&gt; column to it.&lt;/p&gt;

&lt;p&gt;Run this migration in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g migration AddPositionToTasks position:integer
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Pro tip: I highly recommend adding the &lt;code&gt;acts_as_list&lt;/code&gt; gem to your Gemfile. It handles all the annoying math of shifting positions up and down in the database automatically.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you use the gem, just add this to your model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/task.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Task&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;acts_as_list&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: Pin SortableJS (Importmap)
&lt;/h2&gt;

&lt;p&gt;Because we are using Importmaps, we do not need to run &lt;code&gt;npm install&lt;/code&gt;. We just pin the library directly from the CDN.&lt;/p&gt;

&lt;p&gt;Run this in your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/importmap pin sortablejs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will automatically add the correct URL to your &lt;code&gt;config/importmap.rb&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: The HTML View
&lt;/h2&gt;

&lt;p&gt;Now let's build the list in our view. We need to wrap our tasks in a &lt;code&gt;div&lt;/code&gt; or &lt;code&gt;ul&lt;/code&gt; and attach a Stimulus controller to it. We also need to give each item a data attribute so our Javascript knows which Task ID is being dragged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/tasks/index.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My Tasks&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- We attach the 'sortable' controller here --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"sortable"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="vi"&gt;@tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:position&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

    &lt;span class="c"&gt;&amp;lt;!-- We store the task ID on the list item --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&lt;/span&gt; &lt;span class="na"&gt;data-id=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"p-4 bg-white border mb-2 cursor-move"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%&lt;/span&gt; &lt;span class="k"&gt;end&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 4: The Stimulus Controller
&lt;/h2&gt;

&lt;p&gt;Next, we generate our Stimulus controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g stimulus sortable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the newly created file at &lt;code&gt;app/javascript/controllers/sortable_controller.js&lt;/code&gt;. This is where the magic happens. We import &lt;code&gt;SortableJS&lt;/code&gt;, initialize it, and tell it to send a network request to Rails whenever the user drops an item.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/controllers/sortable_controller.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@hotwired/stimulus&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Sortable&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sortablejs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;Controller&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Initialize Sortable on the HTML element this controller is attached to&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sortable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Sortable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;onEnd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;updatePosition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;updatePosition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Get the dragged item's ID and its new index in the list&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;newIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;newIndex&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="c1"&gt;// ActsAsList is 1-indexed, JS is 0-indexed&lt;/span&gt;

    &lt;span class="c1"&gt;// Grab the CSRF token so Rails doesn't block our request&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[name='csrf-token']&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;

    &lt;span class="c1"&gt;// Send an AJAX request to our Rails controller&lt;/span&gt;
    &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/tasks/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/move`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PATCH&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X-CSRF-Token&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;csrfToken&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;newIndex&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 5: The Rails Controller &amp;amp; Route
&lt;/h2&gt;

&lt;p&gt;The Javascript is sending a &lt;code&gt;PATCH&lt;/code&gt; request to &lt;code&gt;/tasks/:id/move&lt;/code&gt;. Let's create that route and the controller action to handle it.&lt;/p&gt;

&lt;p&gt;Update your routes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:tasks&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;member&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;patch&lt;/span&gt; &lt;span class="ss"&gt;:move&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, update the database in your &lt;code&gt;TasksController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/tasks_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TasksController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;move&lt;/span&gt;
    &lt;span class="vi"&gt;@task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# If you are using the acts_as_list gem, it is this simple:&lt;/span&gt;
    &lt;span class="vi"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_at&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:position&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# We don't need to render a view, just tell JS it was successful&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;That's pretty much it! &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We pinned the library via Importmap.&lt;/li&gt;
&lt;li&gt;We initialized &lt;code&gt;SortableJS&lt;/code&gt; in a 10-line Stimulus controller.&lt;/li&gt;
&lt;li&gt;We used a standard &lt;code&gt;fetch&lt;/code&gt; request to update the position in the database.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No massive React setups, no JSON API wrappers, and absolutely zero Webpack configurations. Just plain HTML, a tiny bit of Javascript, and standard Rails routing. This is why the modern Hotwire stack is so incredibly fast for building features.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>javascript</category>
      <category>stimulus</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AdonisJS vs Ruby on Rails: Which MVC Framework Wins?</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Wed, 08 Apr 2026 23:12:07 +0000</pubDate>
      <link>https://forem.com/zilton7/adonisjs-vs-ruby-on-rails-which-mvc-framework-wins-4bgn</link>
      <guid>https://forem.com/zilton7/adonisjs-vs-ruby-on-rails-which-mvc-framework-wins-4bgn</guid>
      <description>&lt;p&gt;Very often I see JavaScript developers getting tired of building backend APIs with Express.js. Express is incredibly fast, but it has zero structure. You have to figure out where to put your routes, how to connect to the database, and how to handle authentication completely by yourself.&lt;/p&gt;

&lt;p&gt;Eventually, these developers discover &lt;strong&gt;AdonisJS&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;Adonis is literally famous for being the "Ruby on Rails of Node.js". It gives you a beautiful MVC (Model-View-Controller) structure, an ORM for the database, and everything comes pre-configured. It is, without a doubt, the best backend framework in the JavaScript ecosystem.&lt;/p&gt;

&lt;p&gt;But if it is so good, why do I still use Ruby on Rails in 2026? &lt;/p&gt;

&lt;p&gt;I have built projects with both. While Adonis is a massive upgrade for Node.js developers, here is my honest breakdown of why Rails is still the ultimate tool for getting things done.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The Language: Ruby vs TypeScript
&lt;/h2&gt;

&lt;p&gt;AdonisJS is built entirely with TypeScript. For a lot of people, this is a huge plus. TypeScript catches errors before you even run your code, which is great for massive teams.&lt;/p&gt;

&lt;p&gt;But for a solo developer or a small startup, TypeScript can feel like wearing handcuffs. You have to define types, write interfaces, and constantly satisfy the compiler. It slows down your prototyping.&lt;/p&gt;

&lt;p&gt;Ruby, on the other hand, was built for &lt;strong&gt;Developer Happiness&lt;/strong&gt;. It is expressive and reads almost like plain English.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ruby&lt;/span&gt;
&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ago&lt;/span&gt;
&lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribed?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I write Ruby, I feel like I am just writing down my thoughts. When I write TypeScript, I feel like I am filling out legal paperwork.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The ORM: ActiveRecord vs Lucid
&lt;/h2&gt;

&lt;p&gt;Adonis comes with a fantastic ORM called &lt;strong&gt;Lucid&lt;/strong&gt;. It is heavily inspired by Rails and Laravel. It handles migrations, models, and relationships very well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// adonis (TypeScript)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;is_active&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orderBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created_at&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;desc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is very good. But &lt;strong&gt;ActiveRecord&lt;/strong&gt; in Rails is simply magic. It has been polished for over 20 years. The sheer amount of built-in features, scopes, and association helpers in ActiveRecord makes querying the database effortless.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# rails&lt;/span&gt;
&lt;span class="vi"&gt;@users&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;order&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;created_at: :desc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Plus, Rails handles database migrations slightly better. If you need to roll back a migration, Rails almost always knows how to reverse it automatically. In Adonis, you often have to write the "up" and "down" logic manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. The Frontend Setup
&lt;/h2&gt;

&lt;p&gt;AdonisJS pairs beautifully with &lt;strong&gt;Inertia.js&lt;/strong&gt; (which lets you build your frontend in React, Vue, or Svelte without an API). If you love the Javascript ecosystem, this is a dream setup.&lt;/p&gt;

&lt;p&gt;But it still means you have a complex build step. You still have a &lt;code&gt;package.json&lt;/code&gt; file with 50 dependencies. You still have to wait for Vite or Webpack to compile your frontend code.&lt;/p&gt;

&lt;p&gt;Rails 8 takes a completely different path. With &lt;strong&gt;Hotwire&lt;/strong&gt; and &lt;strong&gt;Importmaps&lt;/strong&gt;, Rails eliminates the build step entirely. &lt;/p&gt;

&lt;p&gt;You write standard HTML (ERB) views, and Hotwire makes the page feel as fast as a React app without writing any custom JavaScript. If you want to add a library, you just pin it with Importmaps. Your computer's hard drive isn't clogged with massive &lt;code&gt;node_modules&lt;/code&gt; folders, and your app boots instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The Ecosystem and "The Rails Way"
&lt;/h2&gt;

&lt;p&gt;The NPM (Node Package Manager) registry is huge. You can find a package for literally anything. &lt;br&gt;
The problem? A lot of NPM packages are tiny, abandoned, or don't play nicely together. &lt;/p&gt;

&lt;p&gt;In the Ruby world, gems are usually built specifically for Rails. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Need authentication? Use &lt;code&gt;devise&lt;/code&gt; or &lt;code&gt;has_secure_password&lt;/code&gt;. &lt;/li&gt;
&lt;li&gt;  Need an admin panel? Use &lt;code&gt;avo&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  Need background jobs? Rails 8 has &lt;code&gt;solid_queue&lt;/code&gt; built right in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because Rails enforces "The Rails Way" (Convention over Configuration), almost every gem plugs into your app perfectly. You don't have to waste a weekend writing glue code to make a library work with your framework. &lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: Which one should you use?
&lt;/h2&gt;

&lt;p&gt;I have a lot of respect for AdonisJS. It brings much-needed sanity to the chaotic Node.js world. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Choose AdonisJS&lt;/strong&gt; if you already know TypeScript, you absolutely love React/Vue, and you want to use a single language (Javascript) across your entire stack. It is the best choice in the Node ecosystem.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Choose Ruby on Rails&lt;/strong&gt; if you value your time above everything else. If you are a solo founder, an indie hacker, or you just want to take an idea and turn it into a profitable product as fast as humanly possible, Rails is still undefeated. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I tried the "Rails of Node", but it turns out, I just prefer the real thing.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>node</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Accept Crypto Payments in Your Rails 8 App</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Tue, 07 Apr 2026 23:11:38 +0000</pubDate>
      <link>https://forem.com/zilton7/how-to-accept-crypto-payments-in-your-rails-8-app-10eb</link>
      <guid>https://forem.com/zilton7/how-to-accept-crypto-payments-in-your-rails-8-app-10eb</guid>
      <description>&lt;p&gt;When users reach out asking: &lt;em&gt;"Can I pay with Bitcoin or USDC?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the past, my answer was always no. The thought of setting up a Bitcoin node, managing private keys, and constantly checking the blockchain for confirmations sounded like an absolute nightmare for a solo developer. &lt;/p&gt;

&lt;p&gt;But in 2026, accepting crypto is exactly like accepting credit cards. You do not need to touch the blockchain directly. You just use a payment gateway (like Coinbase Commerce, NowPayments, or BTCPay Server) that handles the wallet generation and gives you a simple REST API and webhooks.&lt;/p&gt;

&lt;p&gt;Here is how to integrate a crypto payment gateway into your Rails 8 app in 4 easy steps. I will use the standard API approach, which works for almost any major crypto provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Database Setup
&lt;/h2&gt;

&lt;p&gt;First off, we need a way to track the payment. When a user clicks "Pay with Crypto", the gateway will generate a unique payment ID. We need to save this ID in our database so we can update the order when the user actually sends the funds.&lt;/p&gt;

&lt;p&gt;Let's generate a simple &lt;code&gt;Order&lt;/code&gt; model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails g model Order user:references amount_in_cents:integer status:string crypto_charge_id:string
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our model, we can set a default status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/order.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Order&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;

  &lt;span class="c1"&gt;# Statuses: pending, unconfirmed, completed, failed&lt;/span&gt;
  &lt;span class="n"&gt;after_initialize&lt;/span&gt; &lt;span class="ss"&gt;:set_default_status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;if: :new_record?&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_default_status&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="s1"&gt;'pending'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: Creating the Charge (The API Call)
&lt;/h2&gt;

&lt;p&gt;When the user clicks the checkout button, we need to tell our crypto provider to generate a payment page with the exact amount and currency. &lt;/p&gt;

&lt;p&gt;We don't need a heavy SDK for this. We can just use the &lt;code&gt;http&lt;/code&gt; gem to make a fast POST request to the provider's API (in this example, I'll use the Coinbase Commerce API structure, as it is the industry standard).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/crypto_checkouts_controller.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'http'&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CryptoCheckoutsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;amount_in_cents: &lt;/span&gt;&lt;span class="mi"&gt;50_00&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# $50.00&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Call the Crypto Gateway API&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;"X-CC-Api-Key"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'CRYPTO_API_KEY'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="s2"&gt;"X-CC-Version"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"2018-03-22"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"Content-Type"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://api.commerce.coinbase.com/charges"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"Pro Subscription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;description: &lt;/span&gt;&lt;span class="s2"&gt;"One year of Pro access"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;local_price: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="s2"&gt;"50.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;currency: &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="ss"&gt;pricing_type: &lt;/span&gt;&lt;span class="s2"&gt;"fixed_price"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;metadata: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;order_id: &lt;/span&gt;&lt;span class="vi"&gt;@order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="c1"&gt;# We pass our internal ID here!&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Parse the response&lt;/span&gt;
    &lt;span class="n"&gt;charge_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Save their unique charge ID&lt;/span&gt;
    &lt;span class="vi"&gt;@order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;crypto_charge_id: &lt;/span&gt;&lt;span class="n"&gt;charge_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# 4. Redirect the user to the generated Crypto payment page&lt;/span&gt;
    &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;charge_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"hosted_url"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;allow_other_host: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 3: The View
&lt;/h2&gt;

&lt;p&gt;This is the easiest part. You don't need to build a complex UI with QR codes. The gateway handles that for you. You just need a button.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- app/views/pricing/index.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"pricing-card"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Pro Plan - $50&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Credit Card Button --&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Pay with Stripe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stripe_checkout_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- Crypto Button --&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;button_to&lt;/span&gt; &lt;span class="s2"&gt;"Pay with Crypto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;crypto_checkouts_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="s2"&gt;"btn-crypto"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the user clicks this, they are redirected to a secure, hosted page where they can pick their coin (BTC, ETH, USDC), scan the QR code with their wallet, and send the money.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 4: The Webhook (The Magic)
&lt;/h2&gt;

&lt;p&gt;Crypto transactions take time. Bitcoin can take 10 minutes to confirm. Because of this, the user might close their browser before the payment finishes. &lt;/p&gt;

&lt;p&gt;To solve this, the crypto gateway will send a background &lt;strong&gt;Webhook&lt;/strong&gt; (a POST request) to your Rails app the moment the blockchain confirms the money has arrived.&lt;/p&gt;

&lt;p&gt;We need to create a controller to catch this request, verify it is actually from the gateway (and not a hacker), and update our order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/controllers/webhooks/crypto_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Webhooks::CryptoController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;# We must skip the CSRF token check because this request comes from an external server&lt;/span&gt;
  &lt;span class="n"&gt;skip_before_action&lt;/span&gt; &lt;span class="ss"&gt;:verify_authenticity_token&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
    &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;
    &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'X-CC-Webhook-Signature'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# 1. Verify the signature (Crucial Security Step!)&lt;/span&gt;
    &lt;span class="c1"&gt;# We use OpenSSL to generate a hash using our secret key and the payload&lt;/span&gt;
    &lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Digest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'sha256'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;computed_signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenSSL&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HMAC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'CRYPTO_WEBHOOK_SECRET'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SecurityUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;secure_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;computed_signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;plain: &lt;/span&gt;&lt;span class="s2"&gt;"Invalid signature"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# 2. Process the event&lt;/span&gt;
    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;event_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# The gateway passes back the metadata we gave it in Step 2&lt;/span&gt;
    &lt;span class="n"&gt;order_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;event_type&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"charge:pending"&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'unconfirmed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"charge:confirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"charge:resolved"&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'completed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# Here you would trigger an email or grant access to the product&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="s2"&gt;"charge:failed"&lt;/span&gt;
        &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;status: &lt;/span&gt;&lt;span class="s1"&gt;'failed'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# 3. Always return a 200 OK so the gateway knows we got the message&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="ss"&gt;:ok&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't forget to add the route for this webhook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:webhooks&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="s1"&gt;'crypto'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'crypto#create'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;That's pretty much it! Integrating cryptocurrency payments into Rails is no different than integrating Stripe or PayPal. &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You create an order in your database.&lt;/li&gt;
&lt;li&gt;You ask the API for a checkout URL.&lt;/li&gt;
&lt;li&gt;You redirect the user.&lt;/li&gt;
&lt;li&gt;You wait for the Webhook to tell you the payment was successful.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By using a hosted gateway, you completely avoid the legal and technical nightmares of holding private keys or managing blockchain nodes. You just write clean Ruby code and let the provider handle the heavy lifting.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>crypto</category>
      <category>payments</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The Ultimate Guide to Universal Linux Apps: Snap, Flatpak, and AppImage</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Mon, 06 Apr 2026 23:11:51 +0000</pubDate>
      <link>https://forem.com/zilton7/the-ultimate-guide-to-universal-linux-apps-snap-flatpak-and-appimage-a50</link>
      <guid>https://forem.com/zilton7/the-ultimate-guide-to-universal-linux-apps-snap-flatpak-and-appimage-a50</guid>
      <description>&lt;p&gt;Very often I find myself remembering the "bad old days" of Linux. If you wanted to install a simple app, you had to add a random PPA repository, run &lt;code&gt;apt-get update&lt;/code&gt;, and pray that it didn't break your system dependencies. If you used Arch Linux instead of Ubuntu, you had to hope someone made an AUR package for it.&lt;/p&gt;

&lt;p&gt;Today, the Linux desktop is much better. We have "Universal Package Managers". They bundle the app and all its dependencies into one single package that runs on any Linux distribution. &lt;/p&gt;

&lt;p&gt;But now we have a new problem. There are three competing standards: &lt;strong&gt;Snap, Flatpak, and AppImage&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;I have used all of them extensively over the years. Here is my honest breakdown of how they work, the pros and cons of each, and which one you should actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. AppImage (The Portable USB Drive)
&lt;/h2&gt;

&lt;p&gt;AppImage is the simplest of the three. It is the closest thing Linux has to a Windows &lt;code&gt;.exe&lt;/code&gt; file or a macOS &lt;code&gt;.dmg&lt;/code&gt; file. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
You don't actually "install" an AppImage. You just go to a website, download a single file, make it executable, and double-click it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download the file&lt;/span&gt;
wget https://example.com/cool-app.AppImage

&lt;span class="c"&gt;# Make it executable&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x cool-app.AppImage

&lt;span class="c"&gt;# Run it&lt;/span&gt;
./cool-app.AppImage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  You don't need root (sudo) privileges to run it.&lt;/li&gt;
&lt;li&gt;  You can put it on a USB drive and run it on any Linux computer instantly.&lt;/li&gt;
&lt;li&gt;  It leaves your system completely clean. If you want to delete the app, you just delete the file.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Bad:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  There is no central "App Store" to update them. If a new version comes out, you have to go to the website and download the new file manually.&lt;/li&gt;
&lt;li&gt;  It doesn't automatically add a shortcut to your desktop menu (unless you install a third-party tool like AppImageLauncher).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Snap (The Corporate Monolith)
&lt;/h2&gt;

&lt;p&gt;Snap was created by Canonical (the company behind Ubuntu). It was designed to solve package management for both servers and desktop computers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
You install it via the terminal, similar to standard package managers.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;spotify
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  It handles background services and CLI tools very well. If you need to install a database or a server utility, Snap is actually pretty great.&lt;/li&gt;
&lt;li&gt;  Auto-updates are forced in the background, so you are always on the latest version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Bad:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Proprietary Backend:&lt;/strong&gt; The client is open source, but the server that hosts the Snaps is closed source and controlled 100% by Canonical. The Linux community generally hates this.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Clutter:&lt;/strong&gt; Snaps mount themselves as virtual hard drives. If you type &lt;code&gt;lsblk&lt;/code&gt; in your terminal, you will see a massive, ugly list of "loop devices" clogging up your screen.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Performance:&lt;/strong&gt; Historically, Snaps have been very slow to start up. They have improved recently, but they still feel heavier than the alternatives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Flatpak (The Community Winner)
&lt;/h2&gt;

&lt;p&gt;Flatpak was developed with backing from Red Hat. Unlike Snap, it was built specifically and exclusively for &lt;strong&gt;Desktop GUI applications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;br&gt;
You add the Flathub repository, and then you can install apps either through your graphical software center or the terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install an app&lt;/span&gt;
flatpak &lt;span class="nb"&gt;install &lt;/span&gt;flathub com.spotify.Client

&lt;span class="c"&gt;# Run the app&lt;/span&gt;
flatpak run com.spotify.Client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Decentralized:&lt;/strong&gt; Flathub is the main store, but anyone can host their own Flatpak repository. It is truly open source.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Sandboxing:&lt;/strong&gt; This is the killer feature. Flatpak isolates apps from your main system. An app cannot read your personal files or access your webcam unless you give it permission. &lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Flatseal:&lt;/strong&gt; There is a fantastic GUI app called &lt;code&gt;Flatseal&lt;/code&gt; that lets you toggle permissions (like Network, Filesystem, Microphone) for every Flatpak app with simple switches.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The Bad:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Because apps are sandboxed, sometimes they struggle to integrate with system themes or custom cursors.&lt;/li&gt;
&lt;li&gt;  File sizes can be large at first, because it has to download shared "runtimes" (like the GNOME or KDE base files). However, once you have the runtimes, future apps install very fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary: Which one should you use?
&lt;/h2&gt;

&lt;p&gt;If you are setting up a Linux workstation for development or daily use, here is the golden rule I follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Use Flatpak as your default.&lt;/strong&gt; If a GUI app like Discord, Spotify, or VSCode is available on Flathub, use the Flatpak version. It is secure, updates easily, and respects your system.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Use AppImage for quick tests.&lt;/strong&gt; If I just need to use a tool once (like a crypto wallet or a specialized video editor) and don't want to install it permanently, I grab the AppImage.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Avoid Snap unless absolutely necessary.&lt;/strong&gt; Unless I am setting up an Ubuntu server and need a specific CLI tool that is only packaged as a Snap, I remove &lt;code&gt;snapd&lt;/code&gt; from my system entirely.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's pretty much it. The universal package war was messy for a few years, but in 2026, the community has spoken, and &lt;strong&gt;Flatpak&lt;/strong&gt; is the clear winner for the Linux desktop.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>ubuntu</category>
      <category>archlinux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Why Ruby on Rails is the Secret Weapon for AI Startups</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sun, 05 Apr 2026 23:11:49 +0000</pubDate>
      <link>https://forem.com/zilton7/why-ruby-on-rails-is-the-secret-weapon-for-ai-startups-59cd</link>
      <guid>https://forem.com/zilton7/why-ruby-on-rails-is-the-secret-weapon-for-ai-startups-59cd</guid>
      <description>&lt;p&gt;Everyone knows that Python is the king of training AI models. But if you want to actually build a web app that &lt;em&gt;uses&lt;/em&gt; AI (like an AI copywriter, a smart chatbot, or a document analyzer), you don't need Python. You need a web framework.&lt;/p&gt;

&lt;p&gt;Very often I see developers jumping into complex JavaScript setups (like Next.js + a separate backend) just to build a simple AI wrapper. They spend a week just gluing the database, the API, and the frontend together.&lt;/p&gt;

&lt;p&gt;In 2026, building AI apps is all about speed. And surprisingly, the best tool for the AI era is one of the oldest: &lt;strong&gt;Ruby on Rails&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is why Rails is the absolute best framework for building AI products today.&lt;/p&gt;

&lt;h2&gt;
  
  
  REASON 1: AI Loves Conventions
&lt;/h2&gt;

&lt;p&gt;If you use AI tools like Cursor, GitHub Copilot, or ChatGPT to write code, you know they can sometimes get confused. If your project has a custom folder structure and weird configuration files, the AI will hallucinate and put code in the wrong place.&lt;/p&gt;

&lt;p&gt;Rails is built on &lt;strong&gt;Convention over Configuration&lt;/strong&gt;. &lt;br&gt;
Every Rails app looks exactly the same. Models go in &lt;code&gt;app/models&lt;/code&gt;, controllers go in &lt;code&gt;app/controllers&lt;/code&gt;, and database changes happen in &lt;code&gt;db/migrate&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Because Rails is so standardized and has been around for 20 years, AI models are incredibly good at writing Rails code. If you tell an AI, &lt;em&gt;"Create a User model that has many Documents"&lt;/em&gt;, it knows exactly what to do. It generates the perfect migration, the perfect model associations, and the perfect routes without you having to explain your folder structure.&lt;/p&gt;
&lt;h2&gt;
  
  
  REASON 2: AI Calls are Slow (Solid Queue)
&lt;/h2&gt;

&lt;p&gt;When you make a request to OpenAI or Anthropic, it takes time. Sometimes it takes 2 seconds, sometimes it takes 15 seconds. &lt;/p&gt;

&lt;p&gt;If you put that API call directly inside your web controller, your app will freeze. The user will sit there staring at a loading spinner, and the browser might even timeout. You &lt;strong&gt;must&lt;/strong&gt; use background jobs for AI.&lt;/p&gt;

&lt;p&gt;In other frameworks, setting up background workers is a headache. You have to install Redis, configure workers, and manage separate processes.&lt;/p&gt;

&lt;p&gt;In Rails 8, background jobs are built-in by default using &lt;strong&gt;Solid Queue&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/jobs/generate_summary_job.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;GenerateSummaryJob&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationJob&lt;/span&gt;
  &lt;span class="n"&gt;queue_as&lt;/span&gt; &lt;span class="ss"&gt;:default&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Call the slow AI API&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;OpenAiClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate_summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;summary: &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just call &lt;code&gt;GenerateSummaryJob.perform_later(@document.id)&lt;/code&gt; in your controller, and Rails handles the rest perfectly.&lt;/p&gt;

&lt;h2&gt;
  
  
  REASON 3: Real-Time Streaming (Hotwire)
&lt;/h2&gt;

&lt;p&gt;When using AI, users expect to see the text typing out on the screen chunk by chunk, just like ChatGPT. &lt;/p&gt;

&lt;p&gt;To do this in React or Vue, you usually have to set up WebSockets, manage complex frontend state, and write a lot of boilerplate code to append text to a &lt;code&gt;div&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With Rails and &lt;strong&gt;Hotwire&lt;/strong&gt; (Turbo Streams + ActionCable), this is ridiculously easy. You don't need to write any custom JavaScript. &lt;/p&gt;

&lt;p&gt;When your background job gets a chunk of text from the AI, you just broadcast it directly to the HTML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Inside your job or service&lt;/span&gt;
&lt;span class="no"&gt;Turbo&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;StreamsChannel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;broadcast_append_to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"document_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="ss"&gt;target: &lt;/span&gt;&lt;span class="s2"&gt;"ai_output"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
  &lt;span class="ss"&gt;html: &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;ai_text_chunk&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/p&amp;gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser automatically receives the HTML and updates the page instantly. It feels like magic, and it saves you hours of frontend work.&lt;/p&gt;

&lt;h2&gt;
  
  
  REASON 4: The Database is Everything
&lt;/h2&gt;

&lt;p&gt;Most AI apps today use &lt;strong&gt;RAG&lt;/strong&gt; (Retrieval-Augmented Generation). This means you take user data from your database, mix it with a prompt, and send it to the AI.&lt;/p&gt;

&lt;p&gt;To do this well, your database interactions need to be flawless. ActiveRecord is still the most powerful and easiest-to-use ORM in the world. &lt;br&gt;
Need to grab a user's last 5 completed projects and pass their titles to the AI?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;prompt_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="vi"&gt;@user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;completed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pluck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;", "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is one line of plain English. Trying to write that in raw SQL or a clunky JavaScript ORM takes way more mental energy. &lt;/p&gt;

&lt;p&gt;Also, with the &lt;code&gt;neighbor&lt;/code&gt; gem, you can even store and query AI vector embeddings directly inside your standard PostgreSQL database using ActiveRecord.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The AI era is not about inventing new web technologies. It is about taking an AI API and wrapping it in a solid, reliable product. &lt;/p&gt;

&lt;p&gt;As a solo developer, you want to focus 100% of your time on the AI prompt logic and the user experience. Rails gives you the database, the background jobs, the real-time UI, and the structure out of the box. &lt;/p&gt;

&lt;p&gt;That's pretty much it. While everyone else is fighting with Webpack and API endpoints, you can use Rails to launch your AI startup in a weekend.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ai</category>
      <category>ruby</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stop Using RVM: The Ultimate Guide to Ruby Version Managers</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sat, 04 Apr 2026 23:11:50 +0000</pubDate>
      <link>https://forem.com/zilton7/stop-using-rvm-the-ultimate-guide-to-ruby-version-managers-10o7</link>
      <guid>https://forem.com/zilton7/stop-using-rvm-the-ultimate-guide-to-ruby-version-managers-10o7</guid>
      <description>&lt;h1&gt;
  
  
  rbenv vs rvm vs asdf vs mise vs chruby vs direnv
&lt;/h1&gt;

&lt;p&gt;Very often I see beginners getting completely stuck trying to install Ruby on their Mac or Linux machine. You read one tutorial, and it tells you to install RVM. You read another, and it tells you to use rbenv. Then someone on Twitter tells you to use asdf.&lt;/p&gt;

&lt;p&gt;It is very confusing. Why do we even need these tools? &lt;/p&gt;

&lt;p&gt;Because different Rails projects require different Ruby versions. If you try to run a legacy Rails 6 app on Ruby 3.3, it will crash. You need a tool to quickly switch between Ruby versions depending on which project folder you are in.&lt;/p&gt;

&lt;p&gt;Here is my honest breakdown of all the popular version managers, how they work, and which one you should actually use today.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. RVM (The Dinosaur)
&lt;/h2&gt;

&lt;p&gt;RVM (Ruby Version Manager) is the oldest tool on this list. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; It overrides your terminal commands (like &lt;code&gt;cd&lt;/code&gt;) to switch Ruby versions automatically. It also has a feature called "gemsets" to keep your gems separated.&lt;br&gt;
&lt;strong&gt;The Verdict:&lt;/strong&gt; &lt;strong&gt;Do not use this.&lt;/strong&gt; &lt;br&gt;
In the past, RVM was amazing. But today, &lt;code&gt;Bundler&lt;/code&gt; handles gem isolation for us, so "gemsets" are useless. RVM is too heavy, messes with your shell too much, and is generally considered outdated. &lt;/p&gt;
&lt;h2&gt;
  
  
  2. rbenv (The Reliable Standard)
&lt;/h2&gt;

&lt;p&gt;This is probably the most popular choice in the Ruby community right now. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; It uses "shims". A shim is a fake executable. When you type &lt;code&gt;ruby -v&lt;/code&gt;, rbenv catches that command, checks if you have a &lt;code&gt;.ruby-version&lt;/code&gt; file in your current folder, and then passes the command to the correct physical Ruby installation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .ruby-version
3.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Verdict:&lt;/strong&gt; &lt;strong&gt;Highly Recommended.&lt;/strong&gt;&lt;br&gt;
It is lightweight, does exactly one thing, and stays out of your way. If you only write Ruby code, this is a perfectly safe choice.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. chruby (The Minimalist)
&lt;/h2&gt;

&lt;p&gt;If you hate "magic" and fake executables, &lt;code&gt;chruby&lt;/code&gt; is for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; It does not use shims. When you switch Ruby versions, it simply modifies your computer's &lt;code&gt;$PATH&lt;/code&gt; variable to point directly to the correct Ruby folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Terminal command to switch versions&lt;/span&gt;
chruby ruby-3.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Verdict:&lt;/strong&gt; &lt;strong&gt;Great, but manual.&lt;/strong&gt;&lt;br&gt;
It is incredibly fast and clean. The only downside is that it doesn't automatically switch versions when you enter a directory unless you install a secondary tool (like &lt;code&gt;auto.sh&lt;/code&gt; or &lt;code&gt;direnv&lt;/code&gt;).&lt;/p&gt;
&lt;h2&gt;
  
  
  4. asdf (The All-In-One)
&lt;/h2&gt;

&lt;p&gt;Eventually, you will realize you don't just need to manage Ruby. You also need to manage Node.js versions, Python versions, and maybe Postgres versions. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; &lt;code&gt;asdf&lt;/code&gt; uses a plugin system. You install the tool once, and then add plugins for whatever languages you need. It uses a file called &lt;code&gt;.tool-versions&lt;/code&gt; instead of &lt;code&gt;.ruby-version&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# .tool-versions
ruby 3.3.0
nodejs 20.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Verdict:&lt;/strong&gt; &lt;strong&gt;Good, but getting slow.&lt;/strong&gt;&lt;br&gt;
It is extremely useful to have one tool for all languages. However, &lt;code&gt;asdf&lt;/code&gt; is written in Bash, and when you have many plugins, it can actually make your terminal commands noticeably slower. &lt;/p&gt;
&lt;h2&gt;
  
  
  5. mise (The New King)
&lt;/h2&gt;

&lt;p&gt;Previously known as &lt;code&gt;rtx&lt;/code&gt;, this is taking over the developer world right now. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; &lt;code&gt;mise&lt;/code&gt; is basically a clone of &lt;code&gt;asdf&lt;/code&gt;, but it is written in Rust. This makes it blazing fast. It doesn't use shims like rbenv or asdf, it modifies your &lt;code&gt;$PATH&lt;/code&gt; like chruby. &lt;/p&gt;

&lt;p&gt;The best part? It is backwards compatible. It automatically reads your old &lt;code&gt;.ruby-version&lt;/code&gt;, &lt;code&gt;.nvmrc&lt;/code&gt;, and &lt;code&gt;.tool-versions&lt;/code&gt; files and just works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Installing ruby with mise&lt;/span&gt;
mise &lt;span class="nb"&gt;install &lt;/span&gt;ruby@3.3.0
mise use ruby@3.3.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Verdict:&lt;/strong&gt; &lt;strong&gt;The Best Choice Today.&lt;/strong&gt;&lt;br&gt;
If you are setting up a new computer, install &lt;code&gt;mise&lt;/code&gt;. It manages all your languages, it is incredibly fast, and you don't have to deal with slow terminal boot times.&lt;/p&gt;
&lt;h2&gt;
  
  
  6. direnv (The Sidekick)
&lt;/h2&gt;

&lt;p&gt;I added this to the list because people often get confused by it. &lt;code&gt;direnv&lt;/code&gt; is &lt;strong&gt;not&lt;/strong&gt; a Ruby version manager. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; It is an environment variable manager. You put a &lt;code&gt;.envrc&lt;/code&gt; file in your project folder, and when you &lt;code&gt;cd&lt;/code&gt; into that folder, &lt;code&gt;direnv&lt;/code&gt; automatically loads those variables into your terminal (like API keys or Database URLs).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .envrc&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;STRIPE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"sk_test_123"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People often pair &lt;code&gt;direnv&lt;/code&gt; with &lt;code&gt;chruby&lt;/code&gt; or &lt;code&gt;rbenv&lt;/code&gt; to trigger the Ruby version switch automatically. But if you use &lt;code&gt;mise&lt;/code&gt;, you don't really need &lt;code&gt;direnv&lt;/code&gt; anymore, because &lt;code&gt;mise&lt;/code&gt; manages environment variables too!&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary: What should you pick?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  If you want the industry standard and &lt;strong&gt;only care about Ruby&lt;/strong&gt;: Use &lt;strong&gt;rbenv&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  If you want to manage Ruby, Node, and Postgres and want the &lt;strong&gt;fastest tool available&lt;/strong&gt;: Use &lt;strong&gt;mise&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;  If you have &lt;strong&gt;RVM&lt;/strong&gt; installed: Uninstall it and pick one of the two above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's pretty much it. Setting up your environment is annoying, but once you pick a modern tool like &lt;code&gt;mise&lt;/code&gt; or &lt;code&gt;rbenv&lt;/code&gt;, you never have to think about it again.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>tools</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
