<?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>DEV Community: Zil Norvilis</title>
    <description>The latest articles on DEV Community by Zil Norvilis (@zilton7).</description>
    <link>https://dev.to/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>DEV Community: Zil Norvilis</title>
      <link>https://dev.to/zilton7</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zilton7"/>
    <language>en</language>
    <item>
      <title>How Turbo 8 Morphing Makes Rails Frontend Development Feel Like Magic</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Mon, 20 Apr 2026 23:22:43 +0000</pubDate>
      <link>https://dev.to/zilton7/how-turbo-8-morphing-makes-rails-frontend-development-feel-like-magic-am0</link>
      <guid>https://dev.to/zilton7/how-turbo-8-morphing-makes-rails-frontend-development-feel-like-magic-am0</guid>
      <description>&lt;p&gt;When Hotwire first came out, it felt like a superpower. We could finally update our web pages without full browser reloads, and we didn't have to write a single line of React or Vue.&lt;/p&gt;

&lt;p&gt;But after building a few apps with it, a new problem appeared. &lt;/p&gt;

&lt;p&gt;To update the page dynamically, we had to write &lt;strong&gt;Turbo Streams&lt;/strong&gt;. If a user added a new comment to a post, you had to write a specific &lt;code&gt;.turbo_stream.erb&lt;/code&gt; file that told the browser: &lt;em&gt;"Find the div with the ID of &lt;code&gt;comments_list&lt;/code&gt;, and append this new HTML to the bottom of it. Oh, and also find the &lt;code&gt;comments_counter&lt;/code&gt; div and replace it."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Very often I found myself writing tons of these little manual instructions. It started to feel just as annoying as writing custom JavaScript. &lt;/p&gt;

&lt;p&gt;With the release of &lt;strong&gt;Turbo 8&lt;/strong&gt;, the Rails team solved this completely. They introduced &lt;strong&gt;Page Morphing&lt;/strong&gt;. It allows you to delete almost all of your Turbo Stream files and go back to writing plain, simple Rails controllers. &lt;/p&gt;

&lt;p&gt;Here is exactly how it works and how to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with Turbo Streams
&lt;/h2&gt;

&lt;p&gt;In the old way (Turbo 7), if you wanted to like a post without losing your scroll position, your controller looked like 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/likes_controller.rb&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;@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;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;:post_id&lt;/span&gt;&lt;span class="p"&gt;])&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;likes&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;user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;respond_to&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# You had to explicitly render a stream&lt;/span&gt;
    &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;turbo_stream&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 you had to create a matching 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/likes/create.turbo_stream.erb --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_stream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt; &lt;span class="s2"&gt;"post_&lt;/span&gt;&lt;span class="si"&gt;#{&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;id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_likes"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;partial: &lt;/span&gt;&lt;span class="s2"&gt;"likes/count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;locals: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;post: &lt;/span&gt;&lt;span class="vi"&gt;@post&lt;/span&gt; &lt;span class="p"&gt;}&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 is fine for one button. But if an action updates 5 different parts of the screen (the sidebar, the navbar, the main content), you have to write 5 different stream instructions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Page Morphing?
&lt;/h2&gt;

&lt;p&gt;Page Morphing asks a very simple question: &lt;strong&gt;What if we just reload the entire page, but the browser is smart enough to only update the pixels that actually changed?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Under the hood, Turbo 8 uses a library called &lt;code&gt;idiomorph&lt;/code&gt;. When your server sends back a fresh HTML page, the browser compares the new HTML to the old HTML currently on your screen. It finds the differences, and &lt;em&gt;smoothly morphs&lt;/em&gt; the DOM. &lt;/p&gt;

&lt;p&gt;It does not blink. It does not lose your scroll position. It does not delete the text you are currently typing in an input box. It just updates the data seamlessly.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: Enabling Morphing
&lt;/h2&gt;

&lt;p&gt;To use this magic, you don't need complex JavaScript. You just need to add two &lt;code&gt;meta&lt;/code&gt; tags to the &lt;code&gt;&amp;lt;head&amp;gt;&lt;/code&gt; of your application layout.&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/layouts/application.html.erb --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- ... other tags ... --&amp;gt;&lt;/span&gt;

  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;turbo_refreshes_with&lt;/span&gt; &lt;span class="ss"&gt;method: :morph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scroll: :preserve&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Turbo: &lt;em&gt;"Whenever a form is submitted or a link is clicked, don't do a hard page replace. Morph the page, and keep my scroll position exactly where it is."&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: Simplifying the Controller
&lt;/h2&gt;

&lt;p&gt;Now that morphing is turned on, we can delete our &lt;code&gt;.turbo_stream.erb&lt;/code&gt; file entirely. &lt;/p&gt;

&lt;p&gt;Our controller goes back to looking like a classic, boring Rails 4 controller. We just redirect back to the page!&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/likes_controller.rb&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;@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;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;:post_id&lt;/span&gt;&lt;span class="p"&gt;])&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;likes&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;user: &lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;# No more respond_to. Just redirect.&lt;/span&gt;
  &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;posts_path&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the redirect happens, Rails sends the fresh HTML for the &lt;code&gt;posts_path&lt;/code&gt;. Turbo catches it, diffs it against your current screen, and updates the like counter instantly. Your user doesn't even notice the page reloaded. &lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 3: Real-Time Magic (&lt;code&gt;broadcasts_refreshes&lt;/code&gt;)
&lt;/h2&gt;

&lt;p&gt;This is where it gets absolutely insane. &lt;/p&gt;

&lt;p&gt;What if you want the screen to update when &lt;em&gt;someone else&lt;/em&gt; likes the post? Like a real-time WebSocket update?&lt;/p&gt;

&lt;p&gt;In the old days, you had to use &lt;code&gt;broadcast_replace_to&lt;/code&gt; inside your model. Now, you just add one line to your ActiveRecord 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/like.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Like&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;:post&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;# This replaces all manual broadcasting!&lt;/span&gt;
  &lt;span class="n"&gt;broadcasts_refreshes&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a new Like is created in the database, Rails automatically sends a tiny WebSocket signal to anyone currently looking at that post. The signal simply says: &lt;em&gt;"Hey, something changed. Please refresh your page."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The user's browser silently fetches the new HTML in the background and morphs the screen. The new like appears instantly. You achieved full real-time reactivity with &lt;strong&gt;one line of Ruby code&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;As a solo developer, your goal is to write as little code as possible while delivering the best possible user experience. &lt;/p&gt;

&lt;p&gt;Manual Turbo Streams (&lt;code&gt;append&lt;/code&gt;, &lt;code&gt;replace&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;) are still useful for very specific, complex animations. But for 95% of your application, you should be using &lt;strong&gt;Morphing&lt;/strong&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the &lt;code&gt;&amp;lt;%= turbo_refreshes_with %&amp;gt;&lt;/code&gt; tag to your layout.&lt;/li&gt;
&lt;li&gt;Delete your &lt;code&gt;.turbo_stream.erb&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;redirect_to&lt;/code&gt; in your controllers.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;broadcasts_refreshes&lt;/code&gt; in your models for real-time updates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Stop trying to manually manage the state of your HTML. Let the browser do the heavy lifting for you.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>hotwire</category>
      <category>frontend</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Add a Visual Page Builder to Your Rails App in 10 Minutes</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sun, 19 Apr 2026 23:22:39 +0000</pubDate>
      <link>https://dev.to/zilton7/how-to-add-a-visual-page-builder-to-your-rails-app-in-10-minutes-52im</link>
      <guid>https://dev.to/zilton7/how-to-add-a-visual-page-builder-to-your-rails-app-in-10-minutes-52im</guid>
      <description>&lt;h1&gt;
  
  
  The Quickest Way to Build a Drag-and-Drop Landing Page Builder in Rails 8
&lt;/h1&gt;

&lt;p&gt;Often when I am building a SaaS or a client project, and the client asks for a feature I always dread: &lt;em&gt;"Can I have a page where I can build my own landing pages?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you try to build a drag-and-drop page builder from scratch using pure Ruby and Javascript, it will take you months. You have to handle layout grids, CSS saving, image uploads, and responsive design. &lt;/p&gt;

&lt;p&gt;Instead of reinventing the wheel, the quickest and easiest way to do this in Rails 8 is to embed an open-source library called &lt;strong&gt;GrapesJS&lt;/strong&gt;. It is a massive, production-ready page builder that outputs clean HTML and CSS. We can easily wrap it inside a Stimulus controller and save the output to our Rails database.&lt;/p&gt;

&lt;p&gt;Here is how to add a drag-and-drop landing page builder to your Rails app in 5 steps.&lt;/p&gt;

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

&lt;p&gt;First off, we need a place to save the pages our users create. We don't need a complex schema. A landing page basically just consists of two things: the HTML structure and the CSS styles.&lt;/p&gt;

&lt;p&gt;Let's generate a simple &lt;code&gt;Page&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 Page title:string html_content:text css_content:text
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: Loading GrapesJS
&lt;/h2&gt;

&lt;p&gt;GrapesJS is a heavy library. It requires both Javascript and CSS to work. &lt;/p&gt;

&lt;p&gt;For the Javascript, we can use Rails 8 Importmaps to pin it:&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 grapesjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the CSS, GrapesJS needs its core stylesheet to make the editor look good. The easiest way to include this without fighting the asset pipeline is to just drop the CDN link directly into the view where the editor will live.&lt;/p&gt;

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

&lt;p&gt;Now we create the view where the user will actually build the page. We will create a &lt;code&gt;div&lt;/code&gt; for the editor, and attach a Stimulus controller to it. We also need a "Save" button to send the data back to Rails.&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/pages/edit.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- 1. Load the GrapesJS CSS --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"https://unpkg.com/grapesjs/dist/css/grapes.min.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;data-controller=&lt;/span&gt;&lt;span class="s"&gt;"page-builder"&lt;/span&gt; &lt;span class="na"&gt;data-page-builder-id-value=&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@page&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="nt"&gt;&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;"header"&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;Editing: &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@page&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="c"&gt;&amp;lt;!-- This button triggers the save method in our Stimulus controller --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;data-action=&lt;/span&gt;&lt;span class="s"&gt;"click-&amp;gt;page-builder#save"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Save Page&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- This is the empty canvas where GrapesJS will inject the builder --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"gjs"&lt;/span&gt; &lt;span class="na"&gt;data-page-builder-target=&lt;/span&gt;&lt;span class="s"&gt;"editor"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="c"&gt;&amp;lt;!-- Load existing content if the user is editing a saved page --&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css_content&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_content&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&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;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Next, we generate our Stimulus controller to initialize the builder:&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 page_builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open the file and add the initialization logic. We import GrapesJS, tell it to attach to our &lt;code&gt;#gjs&lt;/code&gt; div, and write a simple &lt;code&gt;fetch&lt;/code&gt; request to save the data.&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/page_builder_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;grapesjs&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;grapesjs&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="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;targets&lt;/span&gt; &lt;span class="o"&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;editor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Number&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 the drag-and-drop builder&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;editor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;grapesjs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;container&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;editorTarget&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;fromElement&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="c1"&gt;// Loads the existing HTML/CSS we put in the view&lt;/span&gt;
      &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;800px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;100%&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;storageManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// We will handle saving to our database manually&lt;/span&gt;

      &lt;span class="c1"&gt;// Enable the default built-in blocks (text, images, columns, etc)&lt;/span&gt;
      &lt;span class="na"&gt;blockManager&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;appendTo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#blocks&lt;/span&gt;&lt;span class="dl"&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;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Grab the clean HTML and CSS generated by the user&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="o"&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;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getHtml&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt; &lt;span class="o"&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;editor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCss&lt;/span&gt;&lt;span class="p"&gt;();&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="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Send it 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;`/pages/&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;idValue&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="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;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; 
          &lt;span class="na"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
          &lt;span class="na"&gt;css_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;css&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;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Page saved successfully!&lt;/span&gt;&lt;span class="dl"&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;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: Rendering the Final Page
&lt;/h2&gt;

&lt;p&gt;Now the user has dragged their blocks, changed the colors, and clicked "Save". The data is in our database.&lt;/p&gt;

&lt;p&gt;When a public visitor goes to view the page, we just need a very simple Rails controller and a view to spit that code back out.&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/pages_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PagesController&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;# ... standard update action ...&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;
    &lt;span class="vi"&gt;@page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Page&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;# Tell Rails to use an empty layout so our standard app navbar &lt;/span&gt;
    &lt;span class="c1"&gt;# doesn't ruin their custom landing page design&lt;/span&gt;
    &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;layout: &lt;/span&gt;&lt;span class="kp"&gt;false&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 the view is literally just two lines of code:&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/pages/show.html.erb --&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Inject the custom CSS the user created --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- Inject the custom HTML --&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="vi"&gt;@page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Building a drag-and-drop builder sounds like a terrifying task, but by combining &lt;strong&gt;GrapesJS&lt;/strong&gt; with a simple &lt;strong&gt;Stimulus&lt;/strong&gt; controller, you can give your users a massive, professional landing page builder in about 10 minutes. &lt;/p&gt;

&lt;p&gt;You don't need React, you don't need Webpack, and you keep all the data safely stored in your own Postgres or SQLite database.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built a Native Android App with Almost Zero Kotlin Experience</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Sat, 18 Apr 2026 23:22:41 +0000</pubDate>
      <link>https://dev.to/zilton7/how-i-built-a-native-android-app-with-zero-kotlin-experience-45o0</link>
      <guid>https://dev.to/zilton7/how-i-built-a-native-android-app-with-zero-kotlin-experience-45o0</guid>
      <description>&lt;h1&gt;
  
  
  Turning a Rails App into an Android App: Testing Hotwire Native
&lt;/h1&gt;

&lt;p&gt;Recently I wanted to test &lt;strong&gt;Hotwire Native&lt;/strong&gt; in the real world. Having a sensitive stomach, I couldn't find a decent FODMAP diet app that was fast, simple, and didn't have annoying ads. &lt;/p&gt;

&lt;p&gt;So, as developers do, I decided to build one myself. &lt;/p&gt;

&lt;p&gt;My goal was to build a simple web app first, and then see how hard it would actually be to wrap it into a real Android application using Hotwire Native. To my surprise, turning this web app into an Android app was incredibly easy.&lt;/p&gt;

&lt;p&gt;Here is the process of how I built it (you can check out the live web version here:&lt;a href="https://fodmap.norvilis.com/" rel="noopener noreferrer"&gt;https://fodmap.norvilis.com/&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Foundation: Building the Rails App
&lt;/h2&gt;

&lt;p&gt;I wanted to keep the backend as simple as possible. I didn't need a massive database, so I used &lt;strong&gt;SQLite&lt;/strong&gt; to store the pre-gathered data of hundreds of food items and their FODMAP levels. SQLite is incredibly fast for this kind of "read-heavy" application.&lt;/p&gt;

&lt;p&gt;For the user interface, I relied heavily on Hotwire to make it feel like a Single Page Application (SPA).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Live Filter:&lt;/strong&gt; I added a search bar at the top. As you type, it uses a Stimulus controller to submit the form automatically, and a &lt;code&gt;Turbo Frame&lt;/code&gt; swaps out the food list instantly without reloading the page.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;The Modal:&lt;/strong&gt; When you click on a food item to read more details, instead of loading a new page, it opens a clean modal popup using Turbo Frames.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It took a few hours, and the web version worked perfectly. But I wanted this on my phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Magic: Enter Hotwire Native
&lt;/h2&gt;

&lt;p&gt;If you build an app in React, to get it on a phone, you usually have to rewrite your UI in React Native. &lt;/p&gt;

&lt;p&gt;With Hotwire Native, you don't rewrite anything. You create a basic "shell" Android app, and you tell it to load your Rails URL. But unlike a standard web-view, Hotwire Native intercepts your clicks and uses &lt;strong&gt;real native mobile transitions&lt;/strong&gt; to move between screens.&lt;/p&gt;

&lt;p&gt;Here is how the Android setup works.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 1: The Android Project
&lt;/h2&gt;

&lt;p&gt;First off, you need to download Android Studio. You create an empty project and add the Hotwire Native dependency to your &lt;code&gt;build.gradle&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gradle"&gt;&lt;code&gt;&lt;span class="k"&gt;dependencies&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dev.hotwire:core:&amp;lt;latest-version&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;implementation&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"dev.hotwire:navigation-fragments:&amp;lt;latest-version&amp;gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 2: Pointing to the Rails App
&lt;/h2&gt;

&lt;p&gt;In Hotwire Native, you configure a &lt;code&gt;Session&lt;/code&gt;. This tells the Android app where your Rails server lives. You just give it the URL of your app.&lt;/p&gt;

&lt;p&gt;Inside your main Kotlin activity file, you set the starting URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="c1"&gt;// MainActivity.kt&lt;/span&gt;
&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MainActivity&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;HotwireActivity&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Bundle&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;enableEdgeToEdge&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;savedInstanceState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;setContentView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;activity_main&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;findViewById&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;View&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main_nav_host&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;applyDefaultImeWindowInsets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;fun&lt;/span&gt; &lt;span class="nf"&gt;navigatorConfigurations&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;listOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nc"&gt;NavigatorConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;startLocation&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"https://fodmap.norvilis.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;navigatorHostId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;main_nav_host&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 3: The Path Configuration
&lt;/h2&gt;

&lt;p&gt;You might have noticed the &lt;code&gt;path.json&lt;/code&gt; file in the code above. This is the secret weapon of Hotwire Native.&lt;/p&gt;

&lt;p&gt;It is a simple JSON file that lives in your Rails app (usually in &lt;code&gt;public/configurations/android_v1.json&lt;/code&gt;). It tells the Android app how different URLs should behave. &lt;/p&gt;

&lt;p&gt;For example, when a user clicks on a food item, I want the Android app to slide the new screen up from the bottom (like a native modal), instead of just pushing it sideways.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"settings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"register_with_account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"rules"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"patterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/new$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/edit$"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"/foods/[0-9]+$"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"modal"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hotwire://fragment/web/modal/sheet"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"patterns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;".*"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"uri"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"hotwire://fragment/web"&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When the Android app sees a URL that matches &lt;code&gt;/foods/1&lt;/code&gt;, it reads this JSON file and says, &lt;em&gt;"Ah! The Rails developer wants this to be a modal."&lt;/em&gt; It then uses native Android animations to open the screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I Love This Approach
&lt;/h2&gt;

&lt;p&gt;I have almost zero experience writing Kotlin or Java. I am a Ruby developer. &lt;/p&gt;

&lt;p&gt;But with Hotwire Native, I didn't need to build an API. I didn't need to write a separate mobile frontend. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  My live search filter worked out of the box because Turbo works the same way inside the mobile wrapper. &lt;/li&gt;
&lt;li&gt;  My SQLite database and Rails controllers served both the web users and the Android users at the same time.&lt;/li&gt;
&lt;li&gt;  If I want to fix a typo or add a new food item, I deploy my Rails app, and the Android app is updated instantly. No app store reviews required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's pretty much it. If you have a Rails app and you have been avoiding making a mobile version because you don't want to learn a new language, you really need to try Hotwire Native. It is the ultimate superpower for a solo developer.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>hotwire</category>
      <category>mobile</category>
      <category>android</category>
    </item>
    <item>
      <title>Making Sense of JavaScript in Rails: Webpack, Rollup, esbuild, and Importmaps</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Fri, 17 Apr 2026 23:21:40 +0000</pubDate>
      <link>https://dev.to/zilton7/making-sense-of-javascript-in-rails-webpack-rollup-esbuild-and-importmaps-2732</link>
      <guid>https://dev.to/zilton7/making-sense-of-javascript-in-rails-webpack-rollup-esbuild-and-importmaps-2732</guid>
      <description>&lt;p&gt;Very often I find myself talking to developers who are incredibly confused by the JavaScript ecosystem in Rails. &lt;/p&gt;

&lt;p&gt;If you run &lt;code&gt;rails new my_app&lt;/code&gt; today, Rails asks you how you want to handle JavaScript. You look at the options - Importmaps, esbuild, Webpack, Rollup - and your brain hurts. You just want to write some code, but first, you have to choose a compilation strategy.&lt;/p&gt;

&lt;p&gt;For a long time, the Rails community was stuck using &lt;code&gt;Webpacker&lt;/code&gt;. It was slow, the configuration files were massive, and upgrading it broke everything. Thankfully, in 2026, we have much better options. &lt;/p&gt;

&lt;p&gt;Here is my honest, simple breakdown of the four major JavaScript tools, how they work, and which one you should actually pick for your next project.&lt;/p&gt;

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

&lt;p&gt;Webpack is the grandfather of modern JavaScript bundlers. If you worked on Rails 5 or 6, you probably have nightmares about the &lt;code&gt;webpacker&lt;/code&gt; gem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; It takes every single JavaScript file, CSS file, and image in your project, builds a massive dependency graph, and compiles it all into one or two giant &lt;code&gt;.js&lt;/code&gt; files.&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;// Webpack loves to bundle absolutely everything, even CSS&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./style.css&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&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;react&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;&lt;strong&gt;The Good:&lt;/strong&gt; It can do literally anything. It has thousands of plugins. If you are building a massive enterprise SPA (Single Page Application) with a dedicated frontend team, Webpack is battle-tested.&lt;br&gt;
&lt;strong&gt;The Bad:&lt;/strong&gt; It is notoriously slow to compile. The configuration files (&lt;code&gt;webpack.config.js&lt;/code&gt;) are almost impossible to understand for a solo developer. &lt;br&gt;
&lt;strong&gt;The Verdict:&lt;/strong&gt; Do not use Webpack for a new Rails project unless you have a very specific, complex legacy requirement.&lt;/p&gt;
&lt;h2&gt;
  
  
  2. Rollup (The Library Builder)
&lt;/h2&gt;

&lt;p&gt;Rollup came out as an alternative to Webpack. It introduced a cool concept called "Tree Shaking" (throwing away code you imported but never actually used to make the file smaller).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt; It creates incredibly small, optimized bundles. &lt;br&gt;
&lt;strong&gt;The Bad:&lt;/strong&gt; It is not really meant for building entire web applications. It was designed to build JavaScript &lt;em&gt;libraries&lt;/em&gt; (like if you are publishing a package to NPM). &lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: While you probably won't use Rollup directly in Rails, it is worth knowing about because modern tools like **Vite&lt;/em&gt;* actually use Rollup under the hood for their production builds.*&lt;br&gt;
&lt;strong&gt;The Verdict:&lt;/strong&gt; Skip it for standard Rails apps.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. esbuild (The Speed Demon)
&lt;/h2&gt;

&lt;p&gt;When Rails 7 dropped Webpacker, they introduced &lt;code&gt;jsbundling-rails&lt;/code&gt; and made &lt;strong&gt;esbuild&lt;/strong&gt; the new star of the show.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; esbuild does exactly what Webpack does (bundles all your JS into one file), but it is written in &lt;strong&gt;Go&lt;/strong&gt; instead of JavaScript.&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;# How you build with esbuild in your terminal&lt;/span&gt;
esbuild app/javascript/&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="nt"&gt;--bundle&lt;/span&gt; &lt;span class="nt"&gt;--sourcemap&lt;/span&gt; &lt;span class="nt"&gt;--outdir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app/assets/builds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The Good:&lt;/strong&gt; It is unbelievably fast. A project that takes Webpack 30 seconds to compile will take esbuild 0.3 seconds. You literally don't even see it compiling. It also perfectly handles JSX if you are writing React code.&lt;br&gt;
&lt;strong&gt;The Bad:&lt;/strong&gt; You still need to install Node.js, manage a &lt;code&gt;package.json&lt;/code&gt;, and deal with the &lt;code&gt;node_modules&lt;/code&gt; black hole on your computer.&lt;br&gt;
&lt;strong&gt;The Verdict:&lt;/strong&gt; If your app uses &lt;strong&gt;React, Vue, or heavily customized Tailwind/PostCSS&lt;/strong&gt;, esbuild is the absolute best choice. It gives you the power of a bundler without the waiting time.&lt;/p&gt;
&lt;h2&gt;
  
  
  4. Importmaps (The Rails Default / No-Build)
&lt;/h2&gt;

&lt;p&gt;This is the default choice for new Rails applications, and it completely changes how frontend development works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; Importmaps do not bundle your code. There is &lt;strong&gt;no build step&lt;/strong&gt;. You do not need Node.js or &lt;code&gt;npm&lt;/code&gt; installed on your computer at all. &lt;/p&gt;

&lt;p&gt;Instead of compiling libraries into one big file, your browser downloads the libraries directly from a fast CDN (Content Delivery Network) when the user loads the page.&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/importmap.rb&lt;/span&gt;
&lt;span class="c1"&gt;# Rails simply maps the name to a CDN URL&lt;/span&gt;
&lt;span class="n"&gt;pin&lt;/span&gt; &lt;span class="s2"&gt;"lodash"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"https://ga.jspm.io/npm:lodash@4.17.21/lodash.js"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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/application.js&lt;/span&gt;
&lt;span class="c1"&gt;// The browser fetches this directly from the CDN!&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&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;lodash&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;&lt;strong&gt;The Good:&lt;/strong&gt; It is the ultimate "One Person Framework" tool. You never have to wait for JavaScript to compile. You hit save in your editor, refresh the browser, and it is instantly there. Your project folder stays tiny.&lt;br&gt;
&lt;strong&gt;The Bad:&lt;/strong&gt; You cannot use JSX or TypeScript, because those languages &lt;em&gt;require&lt;/em&gt; a build step to be converted into plain JavaScript before the browser can read them.&lt;br&gt;
&lt;strong&gt;The Verdict:&lt;/strong&gt; If you are building a standard Rails app using &lt;strong&gt;Hotwire (Turbo + Stimulus)&lt;/strong&gt;, this is the winner. &lt;/p&gt;

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

&lt;p&gt;Don't overcomplicate your stack before you even write your first line of code. Follow this simple rule:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Are you using React, Vue, or TypeScript?&lt;/strong&gt; Use &lt;strong&gt;esbuild&lt;/strong&gt;. It is blazing fast and handles compiling perfectly.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Are you sticking to the Rails "Omakase" menu (Hotwire, Turbo, Stimulus)?&lt;/strong&gt; Use &lt;strong&gt;Importmaps&lt;/strong&gt;. Ditching &lt;code&gt;node_modules&lt;/code&gt; will make your developer life so much happier.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Webpack was great for its time, but in 2026, we value speed and simplicity. &lt;/p&gt;

</description>
      <category>rails</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>frontend</category>
    </item>
    <item>
      <title>How to Build a Custom Affiliate System in Ruby on Rails</title>
      <dc:creator>Zil Norvilis</dc:creator>
      <pubDate>Thu, 16 Apr 2026 23:14:37 +0000</pubDate>
      <link>https://dev.to/zilton7/how-to-build-a-custom-affiliate-system-in-ruby-on-rails-3lkd</link>
      <guid>https://dev.to/zilton7/how-to-build-a-custom-affiliate-system-in-ruby-on-rails-3lkd</guid>
      <description>&lt;p&gt;When you are launching a new SaaS, getting your first users is the hardest part. One of the best ways to grow is to create an affiliate (or referral) program. You basically pay your existing users a commission to bring in new users.&lt;/p&gt;

&lt;p&gt;Very often I see developers jumping straight to third-party tools like Rewardful or PartnerStack. These tools are amazing, but they usually start at $49 to $99 a month. If your app is brand new and making zero money, that is a huge expense.&lt;/p&gt;

&lt;p&gt;Building a basic affiliate tracking system in Rails is actually very easy. You just need to generate a code, track a cookie, and connect two users together. &lt;/p&gt;

&lt;p&gt;Here is how to build your own custom affiliate system in 5 simple steps.&lt;/p&gt;

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

&lt;p&gt;First off, we need to update our database. Our &lt;code&gt;User&lt;/code&gt; model needs two new columns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A unique code that they can share with their friends.&lt;/li&gt;
&lt;li&gt;An ID that points to the person who referred them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's generate the migration:&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 AddAffiliateFieldsToUsers referral_code:string:uniq referred_by_id:integer:index
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;rails db:migrate&lt;/code&gt; to update your database.&lt;/p&gt;

&lt;h2&gt;
  
  
  STEP 2: The User Model Associations
&lt;/h2&gt;

&lt;p&gt;Now we need to tell our User model how these fields work. A user can have many "referred users", and a user can belong to a "referrer". &lt;/p&gt;

&lt;p&gt;We also want to automatically generate a unique &lt;code&gt;referral_code&lt;/code&gt; every time a new user signs up.&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/user.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&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="c1"&gt;# The person who invited this user&lt;/span&gt;
  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:referrer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s1"&gt;'User'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="s1"&gt;'referred_by_id'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;optional: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# The people this user has invited&lt;/span&gt;
  &lt;span class="n"&gt;has_many&lt;/span&gt; &lt;span class="ss"&gt;:referred_users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;class_name: &lt;/span&gt;&lt;span class="s1"&gt;'User'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="s1"&gt;'referred_by_id'&lt;/span&gt;

  &lt;span class="n"&gt;before_create&lt;/span&gt; &lt;span class="ss"&gt;:generate_referral_code&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_referral_code&lt;/span&gt;
    &lt;span class="c1"&gt;# Generates a random 8-character string (e.g. 'aB9xYz2p')&lt;/span&gt;
    &lt;span class="kp"&gt;loop&lt;/span&gt; &lt;span class="k"&gt;do&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;referral_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;alphanumeric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt; &lt;span class="k"&gt;unless&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;exists?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;referral_code: &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;referral_code&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="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 3: Tracking the Clicks (Cookies)
&lt;/h2&gt;

&lt;p&gt;When a user shares their link, it will look something like this: &lt;code&gt;https://myapp.com/?ref=aB9xYz2p&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If someone clicks that link, they might not sign up immediately. They might browse the homepage, read the pricing page, and then sign up 10 minutes later. We need to "remember" who sent them using a browser Cookie.&lt;/p&gt;

&lt;p&gt;We can do this inside our &lt;code&gt;ApplicationController&lt;/code&gt; so it catches the &lt;code&gt;ref&lt;/code&gt; parameter on any page of our website.&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/application_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:track_affiliate_click&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;track_affiliate_click&lt;/span&gt;
    &lt;span class="k"&gt;if&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;:ref&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
      &lt;span class="c1"&gt;# Save the referral code in a cookie that expires in 30 days&lt;/span&gt;
      &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:affiliate_id&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;value: &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;:ref&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="ss"&gt;expires: &lt;/span&gt;&lt;span class="mi"&gt;30&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;from_now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="ss"&gt;domain: :all&lt;/span&gt; &lt;span class="c1"&gt;# This ensures it works across subdomains if you use them&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="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  STEP 4: Applying the Referral on Signup
&lt;/h2&gt;

&lt;p&gt;Now, when a user finally fills out the registration form and clicks "Sign Up", we need to check if they have that cookie. If they do, we link them to the referrer.&lt;/p&gt;

&lt;p&gt;If you are using &lt;strong&gt;Devise&lt;/strong&gt;, you can just override the &lt;code&gt;create&lt;/code&gt; method in your RegistrationsController, or hook into the user creation process. &lt;/p&gt;

&lt;p&gt;Here is a standard controller example:&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/users_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&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;@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="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Check if the user has an affiliate cookie&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:affiliate_id&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
      &lt;span class="n"&gt;referrer&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;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;referral_code: &lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:affiliate_id&lt;/span&gt;&lt;span class="p"&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;referrer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;referrer&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;referrer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="k"&gt;if&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;save&lt;/span&gt;
      &lt;span class="c1"&gt;# Clear the cookie so we don't use it again accidentally&lt;/span&gt;
      &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:affiliate_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;domain: :all&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;]&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;id&lt;/span&gt;
      &lt;span class="n"&gt;redirect_to&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;notice: &lt;/span&gt;&lt;span class="s2"&gt;"Welcome!"&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;:new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&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;h2&gt;
  
  
  STEP 5: Creating the Affiliate Link
&lt;/h2&gt;

&lt;p&gt;That's the entire backend tracking system! Now, you just need to show the user their link so they can copy it and share it.&lt;/p&gt;

&lt;p&gt;In your view (like &lt;code&gt;app/views/dashboards/show.html.erb&lt;/code&gt;), you can add this:&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="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"affiliate-box"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h3&amp;gt;&lt;/span&gt;Earn 20% for every friend you invite!&lt;span class="nt"&gt;&amp;lt;/h3&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Share your unique link:&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;readonly&lt;/span&gt; &lt;span class="na"&gt;value=&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;root_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;ref: &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;referral_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;You have invited &lt;span class="cp"&gt;&amp;lt;%=&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;referred_users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt; friends.&lt;span class="nt"&gt;&amp;lt;/p&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;h2&gt;
  
  
  What about the Payouts?
&lt;/h2&gt;

&lt;p&gt;This tutorial covers the tracking part, which is 90% of the battle. &lt;/p&gt;

&lt;p&gt;For payouts, the easiest way is to use &lt;strong&gt;Stripe&lt;/strong&gt;. When your new user buys a subscription, you can attach their &lt;code&gt;referrer_id&lt;/code&gt; to the Stripe Customer metadata. Then, once a month, you just write a quick Ruby script to loop through your users, see how many active referrals they have, and add account credits or send them money via Stripe Connect (or PayPal).&lt;/p&gt;

&lt;p&gt;Building it yourself takes about 15 minutes, saves you a lot of money on monthly subscriptions, and gives you total control over how your referral system works.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>saas</category>
      <category>tutorial</category>
    </item>
    <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://dev.to/zilton7/it-looks-like-ruby-but-its-not-how-to-understand-elixir-1db5</link>
      <guid>https://dev.to/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://dev.to/zilton7/pundit-vs-cancancan-vs-action-policy-which-rails-auth-gem-wins-1ghc</link>
      <guid>https://dev.to/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://dev.to/zilton7/wise-testing-what-to-test-and-ignore-as-a-solo-rails-developer-515h</link>
      <guid>https://dev.to/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://dev.to/zilton7/stop-ai-spaghetti-enforcing-rails-architecture-in-2026-2fob</link>
      <guid>https://dev.to/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://dev.to/zilton7/how-to-fix-n1-queries-in-rails-like-a-pro-59b7</link>
      <guid>https://dev.to/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://dev.to/zilton7/building-a-world-class-search-engine-in-rails-with-searchkick-3bgf</link>
      <guid>https://dev.to/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://dev.to/zilton7/the-easiest-way-to-add-drag-and-drop-to-your-rails-app-341b</link>
      <guid>https://dev.to/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>
  </channel>
</rss>
