<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://norvilis.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://norvilis.com/" rel="alternate" type="text/html" /><updated>2026-04-08T23:11:49+00:00</updated><id>https://norvilis.com/feed.xml</id><title type="html">DevBlog by Zil Norvilis</title><subtitle>My thoughts on Web Developing, Solopreneurship, Linux and other. Including notes to myself for easier and quicker developing.</subtitle><author><name>Zil Norvilis</name></author><entry><title type="html">AdonisJS vs Ruby on Rails: Which MVC Framework Wins?</title><link href="https://norvilis.com/adonisjs-vs-ruby-on-rails-which-mvc-framework-wins/" rel="alternate" type="text/html" title="AdonisJS vs Ruby on Rails: Which MVC Framework Wins?" /><published>2026-04-09T00:00:00+00:00</published><updated>2026-04-09T00:00:00+00:00</updated><id>https://norvilis.com/adonisjs-vs-ruby-on-rails-which-mvc-framework-wins</id><content type="html" xml:base="https://norvilis.com/adonisjs-vs-ruby-on-rails-which-mvc-framework-wins/"><![CDATA[<p>Very often I see JavaScript developers getting tired of building backend APIs with Express.js. Express is incredibly fast, but it has zero structure. You have to figure out where to put your routes, how to connect to the database, and how to handle authentication completely by yourself.</p>

<p>Eventually, these developers discover <strong>AdonisJS</strong>.</p>

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

<p>But if it is so good, why do I still use Ruby on Rails in 2026?</p>

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

<h2 id="1-the-language-ruby-vs-typescript">1. The Language: Ruby vs TypeScript</h2>

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

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

<p>Ruby, on the other hand, was built for <strong>Developer Happiness</strong>. It is expressive and reads almost like plain English.</p>

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

<p>When I write Ruby, I feel like I am just writing down my thoughts. When I write TypeScript, I feel like I am filling out legal paperwork.</p>

<h2 id="2-the-orm-activerecord-vs-lucid">2. The ORM: ActiveRecord vs Lucid</h2>

<p>Adonis comes with a fantastic ORM called <strong>Lucid</strong>. It is heavily inspired by Rails and Laravel. It handles migrations, models, and relationships very well.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// adonis (TypeScript)</span>
<span class="kd">const</span> <span class="nx">users</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">User</span><span class="p">.</span><span class="nx">query</span><span class="p">()</span>
  <span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="dl">'</span><span class="s1">is_active</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">)</span>
  <span class="p">.</span><span class="nx">orderBy</span><span class="p">(</span><span class="dl">'</span><span class="s1">created_at</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">desc</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

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

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># rails</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">active</span><span class="p">.</span><span class="nf">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span>
</code></pre></div></div>

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

<h2 id="3-the-frontend-setup">3. The Frontend Setup</h2>

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

<p>But it still means you have a complex build step. You still have a <code class="language-plaintext highlighter-rouge">package.json</code> file with 50 dependencies. You still have to wait for Vite or Webpack to compile your frontend code.</p>

<p>Rails 8 takes a completely different path. With <strong>Hotwire</strong> and <strong>Importmaps</strong>, Rails eliminates the build step entirely.</p>

<p>You write standard HTML (ERB) views, and Hotwire makes the page feel as fast as a React app without writing any custom JavaScript. If you want to add a library, you just pin it with Importmaps. Your computer’s hard drive isn’t clogged with massive <code class="language-plaintext highlighter-rouge">node_modules</code> folders, and your app boots instantly.</p>

<h2 id="4-the-ecosystem-and-the-rails-way">4. The Ecosystem and “The Rails Way”</h2>

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

<p>In the Ruby world, gems are usually built specifically for Rails.</p>
<ul>
  <li>Need authentication? Use <code class="language-plaintext highlighter-rouge">devise</code> or <code class="language-plaintext highlighter-rouge">has_secure_password</code>.</li>
  <li>Need an admin panel? Use <code class="language-plaintext highlighter-rouge">avo</code>.</li>
  <li>Need background jobs? Rails 8 has <code class="language-plaintext highlighter-rouge">solid_queue</code> built right in.</li>
</ul>

<p>Because Rails enforces “The Rails Way” (Convention over Configuration), almost every gem plugs into your app perfectly. You don’t have to waste a weekend writing glue code to make a library work with your framework.</p>

<h2 id="summary-which-one-should-you-use">Summary: Which one should you use?</h2>

<p>I have a lot of respect for AdonisJS. It brings much-needed sanity to the chaotic Node.js world.</p>

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

<p>I tried the “Rails of Node”, but it turns out, I just prefer the real thing.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="nodejs" /><category term="javascript" /><category term="webdev" /><summary type="html"><![CDATA[Very often I see JavaScript developers getting tired of building backend APIs with Express.js. Express is incredibly fast, but it has zero structure. You have to figure out where to put your routes, how to connect to the database, and how to handle authentication completely by yourself.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-09-Adonisjs-Vs-Ruby-On-Rails-Which-Mvc-Framework-Wins/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-09-Adonisjs-Vs-Ruby-On-Rails-Which-Mvc-Framework-Wins/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Accept Crypto Payments in Your Rails 8 App</title><link href="https://norvilis.com/how-to-accept-crypto-payments-in-your-rails-8-app/" rel="alternate" type="text/html" title="How to Accept Crypto Payments in Your Rails 8 App" /><published>2026-04-08T00:00:00+00:00</published><updated>2026-04-08T00:00:00+00:00</updated><id>https://norvilis.com/how-to-accept-crypto-payments-in-your-rails-8-app</id><content type="html" xml:base="https://norvilis.com/how-to-accept-crypto-payments-in-your-rails-8-app/"><![CDATA[<p>When users reach out asking: <em>“Can I pay with Bitcoin or USDC?”</em></p>

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

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

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

<h2 id="step-1-the-database-setup">STEP 1: The Database Setup</h2>

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

<p>Let’s generate a simple <code class="language-plaintext highlighter-rouge">Order</code> model:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>rails g model Order user:references amount_in_cents:integer status:string crypto_charge_id:string
rails db:migrate
</code></pre></div></div>

<p>In our model, we can set a default status:</p>

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

  <span class="c1"># Statuses: pending, unconfirmed, completed, failed</span>
  <span class="n">after_initialize</span> <span class="ss">:set_default_status</span><span class="p">,</span> <span class="ss">if: :new_record?</span>

  <span class="k">def</span> <span class="nf">set_default_status</span>
    <span class="nb">self</span><span class="p">.</span><span class="nf">status</span> <span class="o">||=</span> <span class="s1">'pending'</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="step-2-creating-the-charge-the-api-call">STEP 2: Creating the Charge (The API Call)</h2>

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

<p>We don’t need a heavy SDK for this. We can just use the <code class="language-plaintext highlighter-rouge">http</code> gem to make a fast POST request to the provider’s API (in this example, I’ll use the Coinbase Commerce API structure, as it is the industry standard).</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/crypto_checkouts_controller.rb</span>
<span class="nb">require</span> <span class="s1">'http'</span>

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

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

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

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

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

<h2 id="step-3-the-view">STEP 3: The View</h2>

<p>This is the easiest part. You don’t need to build a complex UI with QR codes. The gateway handles that for you. You just need a button.</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/pricing/index.html.erb --&gt;</span>

<span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"pricing-card"</span><span class="nt">&gt;</span>
  <span class="nt">&lt;h2&gt;</span>Pro Plan - $50<span class="nt">&lt;/h2&gt;</span>
  
  <span class="c">&lt;!-- Credit Card Button --&gt;</span>
  <span class="cp">&lt;%=</span> <span class="n">button_to</span> <span class="s2">"Pay with Stripe"</span><span class="p">,</span> <span class="n">stripe_checkout_path</span> <span class="cp">%&gt;</span>

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

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

<h2 id="step-4-the-webhook-the-magic">STEP 4: The Webhook (The Magic)</h2>

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

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

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

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

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

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

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

    <span class="c1"># 2. Process the event</span>
    <span class="n">event</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
    <span class="n">event_type</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s2">"event"</span><span class="p">,</span> <span class="s2">"type"</span><span class="p">)</span>
    
    <span class="c1"># The gateway passes back the metadata we gave it in Step 2</span>
    <span class="n">order_id</span> <span class="o">=</span> <span class="n">event</span><span class="p">.</span><span class="nf">dig</span><span class="p">(</span><span class="s2">"event"</span><span class="p">,</span> <span class="s2">"data"</span><span class="p">,</span> <span class="s2">"metadata"</span><span class="p">,</span> <span class="s2">"order_id"</span><span class="p">)</span>
    <span class="n">order</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">find_by</span><span class="p">(</span><span class="ss">id: </span><span class="n">order_id</span><span class="p">)</span>

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

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

<p>Don’t forget to add the route for this webhook:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/routes.rb</span>
<span class="n">namespace</span> <span class="ss">:webhooks</span> <span class="k">do</span>
  <span class="n">post</span> <span class="s1">'crypto'</span><span class="p">,</span> <span class="ss">to: </span><span class="s1">'crypto#create'</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="summary">Summary</h2>

<p>That’s pretty much it! Integrating cryptocurrency payments into Rails is no different than integrating Stripe or PayPal.</p>

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

<p>By using a hosted gateway, you completely avoid the legal and technical nightmares of holding private keys or managing blockchain nodes. You just write clean Ruby code and let the provider handle the heavy lifting.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="crypto" /><category term="payments" /><category term="tutorial" /><summary type="html"><![CDATA[When users reach out asking: “Can I pay with Bitcoin or USDC?”]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-08-How-To-Accept-Crypto-Payments-In-Your-Rails-8-App/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-08-How-To-Accept-Crypto-Payments-In-Your-Rails-8-App/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Ultimate Guide to Universal Linux Apps: Snap, Flatpak, and AppImage</title><link href="https://norvilis.com/the-ultimate-guide-to-universal-linux-apps-snap-flatpak-and-appimage/" rel="alternate" type="text/html" title="The Ultimate Guide to Universal Linux Apps: Snap, Flatpak, and AppImage" /><published>2026-04-07T00:00:00+00:00</published><updated>2026-04-07T00:00:00+00:00</updated><id>https://norvilis.com/the-ultimate-guide-to-universal-linux-apps-snap-flatpak-and-appimage</id><content type="html" xml:base="https://norvilis.com/the-ultimate-guide-to-universal-linux-apps-snap-flatpak-and-appimage/"><![CDATA[<p>Very often I find myself remembering the “bad old days” of Linux. If you wanted to install a simple app, you had to add a random PPA repository, run <code class="language-plaintext highlighter-rouge">apt-get update</code>, and pray that it didn’t break your system dependencies. If you used Arch Linux instead of Ubuntu, you had to hope someone made an AUR package for it.</p>

<p>Today, the Linux desktop is much better. We have “Universal Package Managers”. They bundle the app and all its dependencies into one single package that runs on any Linux distribution.</p>

<p>But now we have a new problem. There are three competing standards: <strong>Snap, Flatpak, and AppImage</strong>.</p>

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

<h2 id="1-appimage-the-portable-usb-drive">1. AppImage (The Portable USB Drive)</h2>

<p>AppImage is the simplest of the three. It is the closest thing Linux has to a Windows <code class="language-plaintext highlighter-rouge">.exe</code> file or a macOS <code class="language-plaintext highlighter-rouge">.dmg</code> file.</p>

<p><strong>How it works:</strong>
You don’t actually “install” an AppImage. You just go to a website, download a single file, make it executable, and double-click it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Download the file</span>
wget https://example.com/cool-app.AppImage

<span class="c"># Make it executable</span>
<span class="nb">chmod</span> +x cool-app.AppImage

<span class="c"># Run it</span>
./cool-app.AppImage
</code></pre></div></div>

<p><strong>The Good:</strong></p>
<ul>
  <li>You don’t need root (sudo) privileges to run it.</li>
  <li>You can put it on a USB drive and run it on any Linux computer instantly.</li>
  <li>It leaves your system completely clean. If you want to delete the app, you just delete the file.</li>
</ul>

<p><strong>The Bad:</strong></p>
<ul>
  <li>There is no central “App Store” to update them. If a new version comes out, you have to go to the website and download the new file manually.</li>
  <li>It doesn’t automatically add a shortcut to your desktop menu (unless you install a third-party tool like AppImageLauncher).</li>
</ul>

<h2 id="2-snap-the-corporate-monolith">2. Snap (The Corporate Monolith)</h2>

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

<p><strong>How it works:</strong>
You install it via the terminal, similar to standard package managers.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>snap <span class="nb">install </span>spotify
</code></pre></div></div>

<p><strong>The Good:</strong></p>
<ul>
  <li>It handles background services and CLI tools very well. If you need to install a database or a server utility, Snap is actually pretty great.</li>
  <li>Auto-updates are forced in the background, so you are always on the latest version.</li>
</ul>

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

<h2 id="3-flatpak-the-community-winner">3. Flatpak (The Community Winner)</h2>

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

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

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install an app</span>
flatpak <span class="nb">install </span>flathub com.spotify.Client

<span class="c"># Run the app</span>
flatpak run com.spotify.Client
</code></pre></div></div>

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

<p><strong>The Bad:</strong></p>
<ul>
  <li>Because apps are sandboxed, sometimes they struggle to integrate with system themes or custom cursors.</li>
  <li>File sizes can be large at first, because it has to download shared “runtimes” (like the GNOME or KDE base files). However, once you have the runtimes, future apps install very fast.</li>
</ul>

<h2 id="summary-which-one-should-you-use">Summary: Which one should you use?</h2>

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

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

<p>That’s pretty much it. The universal package war was messy for a few years, but in 2026, the community has spoken, and <strong>Flatpak</strong> is the clear winner for the Linux desktop.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="linux" /><category term="ubuntu" /><category term="archlinux" /><category term="productivity" /><summary type="html"><![CDATA[Very often I find myself remembering the “bad old days” of Linux. If you wanted to install a simple app, you had to add a random PPA repository, run apt-get update, and pray that it didn’t break your system dependencies. If you used Arch Linux instead of Ubuntu, you had to hope someone made an AUR package for it.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-07-The-Ultimate-Guide-To-Universal-Linux-Apps-Snap-Flatpak-And-Appimage/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-07-The-Ultimate-Guide-To-Universal-Linux-Apps-Snap-Flatpak-And-Appimage/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Why Ruby on Rails is the Secret Weapon for AI Startups</title><link href="https://norvilis.com/why-ruby-on-rails-is-the-secret-weapon-for-ai-startups/" rel="alternate" type="text/html" title="Why Ruby on Rails is the Secret Weapon for AI Startups" /><published>2026-04-06T00:00:00+00:00</published><updated>2026-04-06T00:00:00+00:00</updated><id>https://norvilis.com/why-ruby-on-rails-is-the-secret-weapon-for-ai-startups</id><content type="html" xml:base="https://norvilis.com/why-ruby-on-rails-is-the-secret-weapon-for-ai-startups/"><![CDATA[<p>Everyone knows that Python is the king of training AI models. But if you want to actually build a web app that <em>uses</em> AI (like an AI copywriter, a smart chatbot, or a document analyzer), you don’t need Python. You need a web framework.</p>

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

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

<p>Here is why Rails is the absolute best framework for building AI products today.</p>

<h2 id="reason-1-ai-loves-conventions">REASON 1: AI Loves Conventions</h2>

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

<p>Rails is built on <strong>Convention over Configuration</strong>. 
Every Rails app looks exactly the same. Models go in <code class="language-plaintext highlighter-rouge">app/models</code>, controllers go in <code class="language-plaintext highlighter-rouge">app/controllers</code>, and database changes happen in <code class="language-plaintext highlighter-rouge">db/migrate</code>.</p>

<p>Because Rails is so standardized and has been around for 20 years, AI models are incredibly good at writing Rails code. If you tell an AI, <em>“Create a User model that has many Documents”</em>, it knows exactly what to do. It generates the perfect migration, the perfect model associations, and the perfect routes without you having to explain your folder structure.</p>

<h2 id="reason-2-ai-calls-are-slow-solid-queue">REASON 2: AI Calls are Slow (Solid Queue)</h2>

<p>When you make a request to OpenAI or Anthropic, it takes time. Sometimes it takes 2 seconds, sometimes it takes 15 seconds.</p>

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

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

<p>In Rails 8, background jobs are built-in by default using <strong>Solid Queue</strong>.</p>

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

  <span class="k">def</span> <span class="nf">perform</span><span class="p">(</span><span class="n">document_id</span><span class="p">)</span>
    <span class="n">document</span> <span class="o">=</span> <span class="no">Document</span><span class="p">.</span><span class="nf">find</span><span class="p">(</span><span class="n">document_id</span><span class="p">)</span>
    
    <span class="c1"># Call the slow AI API</span>
    <span class="n">response</span> <span class="o">=</span> <span class="no">OpenAiClient</span><span class="p">.</span><span class="nf">generate_summary</span><span class="p">(</span><span class="n">document</span><span class="p">.</span><span class="nf">text</span><span class="p">)</span>
    
    <span class="n">document</span><span class="p">.</span><span class="nf">update</span><span class="p">(</span><span class="ss">summary: </span><span class="n">response</span><span class="p">)</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>You just call <code class="language-plaintext highlighter-rouge">GenerateSummaryJob.perform_later(@document.id)</code> in your controller, and Rails handles the rest perfectly.</p>

<h2 id="reason-3-real-time-streaming-hotwire">REASON 3: Real-Time Streaming (Hotwire)</h2>

<p>When using AI, users expect to see the text typing out on the screen chunk by chunk, just like ChatGPT.</p>

<p>To do this in React or Vue, you usually have to set up WebSockets, manage complex frontend state, and write a lot of boilerplate code to append text to a <code class="language-plaintext highlighter-rouge">div</code>.</p>

<p>With Rails and <strong>Hotwire</strong> (Turbo Streams + ActionCable), this is ridiculously easy. You don’t need to write any custom JavaScript.</p>

<p>When your background job gets a chunk of text from the AI, you just broadcast it directly to the HTML:</p>

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

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

<h2 id="reason-4-the-database-is-everything">REASON 4: The Database is Everything</h2>

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

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

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">prompt_data</span> <span class="o">=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">projects</span><span class="p">.</span><span class="nf">completed</span><span class="p">.</span><span class="nf">last</span><span class="p">(</span><span class="mi">5</span><span class="p">).</span><span class="nf">pluck</span><span class="p">(</span><span class="ss">:title</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">", "</span><span class="p">)</span>
</code></pre></div></div>

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

<p>Also, with the <code class="language-plaintext highlighter-rouge">neighbor</code> gem, you can even store and query AI vector embeddings directly inside your standard PostgreSQL database using ActiveRecord.</p>

<h2 id="summary">Summary</h2>

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

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

<p>That’s pretty much it. While everyone else is fighting with Webpack and API endpoints, you can use Rails to launch your AI startup in a weekend.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="ai" /><category term="ruby" /><category term="webdev" /><summary type="html"><![CDATA[Everyone knows that Python is the king of training AI models. But if you want to actually build a web app that uses AI (like an AI copywriter, a smart chatbot, or a document analyzer), you don’t need Python. You need a web framework.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-06-Why-Ruby-On-Rails-Is-The-Secret-Weapon-For-Ai-Startups/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-06-Why-Ruby-On-Rails-Is-The-Secret-Weapon-For-Ai-Startups/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Stop Using RVM: The Ultimate Guide to Ruby Version Managers</title><link href="https://norvilis.com/stop-using-rvm-the-ultimate-guide-to-ruby-version-managers/" rel="alternate" type="text/html" title="Stop Using RVM: The Ultimate Guide to Ruby Version Managers" /><published>2026-04-05T00:00:00+00:00</published><updated>2026-04-05T00:00:00+00:00</updated><id>https://norvilis.com/stop-using-rvm-the-ultimate-guide-to-ruby-version-managers</id><content type="html" xml:base="https://norvilis.com/stop-using-rvm-the-ultimate-guide-to-ruby-version-managers/"><![CDATA[<h1 id="rbenv-vs-rvm-vs-asdf-vs-mise-vs-chruby-vs-direnv">rbenv vs rvm vs asdf vs mise vs chruby vs direnv</h1>

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

<p>It is very confusing. Why do we even need these tools?</p>

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

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

<h2 id="1-rvm-the-dinosaur">1. RVM (The Dinosaur)</h2>

<p>RVM (Ruby Version Manager) is the oldest tool on this list.</p>

<p><strong>How it works:</strong> It overrides your terminal commands (like <code class="language-plaintext highlighter-rouge">cd</code>) to switch Ruby versions automatically. It also has a feature called “gemsets” to keep your gems separated.
<strong>The Verdict:</strong> <strong>Do not use this.</strong> 
In the past, RVM was amazing. But today, <code class="language-plaintext highlighter-rouge">Bundler</code> handles gem isolation for us, so “gemsets” are useless. RVM is too heavy, messes with your shell too much, and is generally considered outdated.</p>

<h2 id="2-rbenv-the-reliable-standard">2. rbenv (The Reliable Standard)</h2>

<p>This is probably the most popular choice in the Ruby community right now.</p>

<p><strong>How it works:</strong> It uses “shims”. A shim is a fake executable. When you type <code class="language-plaintext highlighter-rouge">ruby -v</code>, rbenv catches that command, checks if you have a <code class="language-plaintext highlighter-rouge">.ruby-version</code> file in your current folder, and then passes the command to the correct physical Ruby installation.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># .ruby-version
3.3.0
</code></pre></div></div>

<p><strong>The Verdict:</strong> <strong>Highly Recommended.</strong>
It is lightweight, does exactly one thing, and stays out of your way. If you only write Ruby code, this is a perfectly safe choice.</p>

<h2 id="3-chruby-the-minimalist">3. chruby (The Minimalist)</h2>

<p>If you hate “magic” and fake executables, <code class="language-plaintext highlighter-rouge">chruby</code> is for you.</p>

<p><strong>How it works:</strong> It does not use shims. When you switch Ruby versions, it simply modifies your computer’s <code class="language-plaintext highlighter-rouge">$PATH</code> variable to point directly to the correct Ruby folder.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal command to switch versions</span>
chruby ruby-3.3.0
</code></pre></div></div>

<p><strong>The Verdict:</strong> <strong>Great, but manual.</strong>
It is incredibly fast and clean. The only downside is that it doesn’t automatically switch versions when you enter a directory unless you install a secondary tool (like <code class="language-plaintext highlighter-rouge">auto.sh</code> or <code class="language-plaintext highlighter-rouge">direnv</code>).</p>

<h2 id="4-asdf-the-all-in-one">4. asdf (The All-In-One)</h2>

<p>Eventually, you will realize you don’t just need to manage Ruby. You also need to manage Node.js versions, Python versions, and maybe Postgres versions.</p>

<p><strong>How it works:</strong> <code class="language-plaintext highlighter-rouge">asdf</code> uses a plugin system. You install the tool once, and then add plugins for whatever languages you need. It uses a file called <code class="language-plaintext highlighter-rouge">.tool-versions</code> instead of <code class="language-plaintext highlighter-rouge">.ruby-version</code>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code># .tool-versions
ruby 3.3.0
nodejs 20.0.0
</code></pre></div></div>

<p><strong>The Verdict:</strong> <strong>Good, but getting slow.</strong>
It is extremely useful to have one tool for all languages. However, <code class="language-plaintext highlighter-rouge">asdf</code> is written in Bash, and when you have many plugins, it can actually make your terminal commands noticeably slower.</p>

<h2 id="5-mise-the-new-king">5. mise (The New King)</h2>

<p>Previously known as <code class="language-plaintext highlighter-rouge">rtx</code>, this is taking over the developer world right now.</p>

<p><strong>How it works:</strong> <code class="language-plaintext highlighter-rouge">mise</code> is basically a clone of <code class="language-plaintext highlighter-rouge">asdf</code>, but it is written in Rust. This makes it blazing fast. It doesn’t use shims like rbenv or asdf, it modifies your <code class="language-plaintext highlighter-rouge">$PATH</code> like chruby.</p>

<p>The best part? It is backwards compatible. It automatically reads your old <code class="language-plaintext highlighter-rouge">.ruby-version</code>, <code class="language-plaintext highlighter-rouge">.nvmrc</code>, and <code class="language-plaintext highlighter-rouge">.tool-versions</code> files and just works.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Installing ruby with mise</span>
mise <span class="nb">install </span>ruby@3.3.0
mise use ruby@3.3.0
</code></pre></div></div>

<p><strong>The Verdict:</strong> <strong>The Best Choice Today.</strong>
If you are setting up a new computer, install <code class="language-plaintext highlighter-rouge">mise</code>. It manages all your languages, it is incredibly fast, and you don’t have to deal with slow terminal boot times.</p>

<h2 id="6-direnv-the-sidekick">6. direnv (The Sidekick)</h2>

<p>I added this to the list because people often get confused by it. <code class="language-plaintext highlighter-rouge">direnv</code> is <strong>not</strong> a Ruby version manager.</p>

<p><strong>How it works:</strong> It is an environment variable manager. You put a <code class="language-plaintext highlighter-rouge">.envrc</code> file in your project folder, and when you <code class="language-plaintext highlighter-rouge">cd</code> into that folder, <code class="language-plaintext highlighter-rouge">direnv</code> automatically loads those variables into your terminal (like API keys or Database URLs).</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .envrc</span>
<span class="nb">export </span><span class="nv">STRIPE_KEY</span><span class="o">=</span><span class="s2">"sk_test_123"</span>
</code></pre></div></div>

<p>People often pair <code class="language-plaintext highlighter-rouge">direnv</code> with <code class="language-plaintext highlighter-rouge">chruby</code> or <code class="language-plaintext highlighter-rouge">rbenv</code> to trigger the Ruby version switch automatically. But if you use <code class="language-plaintext highlighter-rouge">mise</code>, you don’t really need <code class="language-plaintext highlighter-rouge">direnv</code> anymore, because <code class="language-plaintext highlighter-rouge">mise</code> manages environment variables too!</p>

<h2 id="summary-what-should-you-pick">Summary: What should you pick?</h2>

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

<p>That’s pretty much it. Setting up your environment is annoying, but once you pick a modern tool like <code class="language-plaintext highlighter-rouge">mise</code> or <code class="language-plaintext highlighter-rouge">rbenv</code>, you never have to think about it again.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="ruby" /><category term="rails" /><category term="tools" /><category term="productivity" /><summary type="html"><![CDATA[rbenv vs rvm vs asdf vs mise vs chruby vs direnv]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-05-Stop-Using-Rvm-The-Ultimate-Guide-To-Ruby-Version-Managers/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-05-Stop-Using-Rvm-The-Ultimate-Guide-To-Ruby-Version-Managers/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">How to Use Rails Magic Methods in Plain Ruby Scripts</title><link href="https://norvilis.com/how-to-use-rails-magic-methods-in-plain-ruby-scripts/" rel="alternate" type="text/html" title="How to Use Rails Magic Methods in Plain Ruby Scripts" /><published>2026-04-04T00:00:00+00:00</published><updated>2026-04-04T00:00:00+00:00</updated><id>https://norvilis.com/how-to-use-rails-magic-methods-in-plain-ruby-scripts</id><content type="html" xml:base="https://norvilis.com/how-to-use-rails-magic-methods-in-plain-ruby-scripts/"><![CDATA[<p>Very often I find myself writing small, standalone Ruby scripts. Maybe it’s a web scraper, a small background worker, or a quick Sinatra API.</p>

<p>I start typing out my code, and naturally, I write something like this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">my_variable</span><span class="p">.</span><span class="nf">present?</span>
  <span class="nb">puts</span> <span class="s2">"We have data!"</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And immediately, my script crashes with <code class="language-plaintext highlighter-rouge">NoMethodError: undefined method 'present?' for nil:NilClass</code>.</p>

<p>This is the moment every Ruby developer realizes a shocking truth: <strong>Methods like <code class="language-plaintext highlighter-rouge">.present?</code>, <code class="language-plaintext highlighter-rouge">.blank?</code>, and <code class="language-plaintext highlighter-rouge">3.days.ago</code> are NOT part of the Ruby language.</strong> They are part of Rails.</p>

<p>But what if you want to use these awesome methods without generating a massive, heavy Rails application?</p>

<p>You can. All of these “magic” methods live inside a single gem called <strong>ActiveSupport</strong>. You can easily extract it and use it in any plain Ruby script. Here is how to do it.</p>

<h2 id="step-1-the-setup">STEP 1: The Setup</h2>

<p>First off, let’s install the gem. If you are using a <code class="language-plaintext highlighter-rouge">Gemfile</code> for your script, just add this:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">source</span> <span class="s1">'https://rubygems.org'</span>

<span class="n">gem</span> <span class="s1">'activesupport'</span>
</code></pre></div></div>

<p>Run <code class="language-plaintext highlighter-rouge">bundle install</code>. If you are just writing a single <code class="language-plaintext highlighter-rouge">.rb</code> file without a Gemfile, you can install it directly in your terminal by running <code class="language-plaintext highlighter-rouge">gem install activesupport</code>.</p>

<h2 id="step-2-the-all-in-approach">STEP 2: The “All In” Approach</h2>

<p>Now open your ruby script. The easiest way to get all the Rails magic is to require the entire ActiveSupport library at the very top of your file.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># script.rb</span>
<span class="nb">require</span> <span class="s1">'active_support/all'</span>

<span class="c1"># Now you can use time helpers!</span>
<span class="nb">puts</span> <span class="mi">3</span><span class="p">.</span><span class="nf">days</span><span class="p">.</span><span class="nf">ago</span>

<span class="c1"># You can check for blank arrays or strings!</span>
<span class="n">empty_array</span> <span class="o">=</span><span class="p">[]</span>
<span class="nb">puts</span> <span class="n">empty_array</span><span class="p">.</span><span class="nf">blank?</span> <span class="c1"># =&gt; true</span>

<span class="c1"># You can format strings!</span>
<span class="nb">puts</span> <span class="s2">"my_custom_class"</span><span class="p">.</span><span class="nf">camelize</span> <span class="c1"># =&gt; "MyCustomClass"</span>
</code></pre></div></div>

<p>This is great, but there is a catch. <code class="language-plaintext highlighter-rouge">active_support/all</code> is <strong>huge</strong>. It loads thousands of methods into memory. If you are building a tiny, fast script, loading the entire library just to use <code class="language-plaintext highlighter-rouge">.blank?</code> is overkill and will make your script boot up much slower.</p>

<h2 id="step-3-the-cherry-picking-approach-recommended">STEP 3: The “Cherry-Picking” Approach (Recommended)</h2>

<p>Instead of loading everything, ActiveSupport allows you to require <em>only</em> the specific extensions you actually need.</p>

<p>ActiveSupport groups its methods by the Ruby class they extend (like String, Integer, Date, Array, etc.).</p>

<p>Here is how you cherry-pick exactly what you want:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># script.rb</span>

<span class="c1"># 1. I only want .blank? and .present?</span>
<span class="nb">require</span> <span class="s1">'active_support/core_ext/object/blank'</span>

<span class="k">if</span> <span class="s2">""</span><span class="p">.</span><span class="nf">blank?</span>
  <span class="nb">puts</span> <span class="s2">"String is empty"</span>
<span class="k">end</span>

<span class="c1"># 2. I only want the time calculation helpers (like 2.days.from_now)</span>
<span class="nb">require</span> <span class="s1">'active_support/core_ext/integer/time'</span>
<span class="nb">require</span> <span class="s1">'active_support/core_ext/numeric/time'</span>

<span class="nb">puts</span> <span class="mi">2</span><span class="p">.</span><span class="nf">weeks</span><span class="p">.</span><span class="nf">from_now</span>

<span class="c1"># 3. I only want string manipulations (like .squish or .pluralize)</span>
<span class="nb">require</span> <span class="s1">'active_support/core_ext/string'</span>

<span class="nb">puts</span> <span class="s2">"  too   much   spacing  "</span><span class="p">.</span><span class="nf">squish</span> <span class="c1"># =&gt; "too much spacing"</span>
<span class="nb">puts</span> <span class="s2">"apple"</span><span class="p">.</span><span class="nf">pluralize</span>               <span class="c1"># =&gt; "apples"</span>
</code></pre></div></div>

<p>By doing this, your script boots up instantly, uses almost zero RAM, but you still get to use your favorite Rails helpers.</p>

<h2 id="my-top-3-activesupport-methods">My Top 3 ActiveSupport Methods</h2>

<p>If you are wondering what else lives inside ActiveSupport, here are a few methods I use all the time in my standalone scripts:</p>

<p><strong>1. Array <code class="language-plaintext highlighter-rouge">.in_groups_of</code></strong>
Perfect for processing large lists of data in batches.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'active_support/core_ext/array/grouping'</span>

<span class="n">users</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">]</span>
<span class="n">users</span><span class="p">.</span><span class="nf">in_groups_of</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">group</span><span class="o">|</span>
  <span class="nb">puts</span> <span class="n">group</span><span class="p">.</span><span class="nf">inspect</span>
<span class="k">end</span>
<span class="c1"># Outputs:[1, 2], then [3, 4], then [5, 6]</span>
</code></pre></div></div>

<p><strong>2. Hash <code class="language-plaintext highlighter-rouge">.with_indifferent_access</code></strong>
Have you ever tried to get a value from a hash using <code class="language-plaintext highlighter-rouge">my_hash[:name]</code> but the key was actually a string <code class="language-plaintext highlighter-rouge">"name"</code>? This fixes that annoyance completely.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'active_support/core_ext/hash/indifferent_access'</span>

<span class="n">data</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"name"</span> <span class="o">=&gt;</span> <span class="s2">"Zil"</span> <span class="p">}.</span><span class="nf">with_indifferent_access</span>
<span class="nb">puts</span> <span class="n">data</span><span class="p">[</span><span class="ss">:name</span><span class="p">]</span> <span class="c1"># =&gt; "Zil" (It works with a symbol too!)</span>
</code></pre></div></div>

<p><strong>3. Enumerable <code class="language-plaintext highlighter-rouge">.pluck</code></strong>
If you have an array of hashes (like an API response), you can easily extract just one specific key from all of them.</p>
<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">require</span> <span class="s1">'active_support/core_ext/enumerable'</span>

<span class="n">api_response</span> <span class="o">=</span><span class="p">[{</span> <span class="ss">id: </span><span class="mi">1</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"Zil"</span> <span class="p">},</span> <span class="p">{</span> <span class="ss">id: </span><span class="mi">2</span><span class="p">,</span> <span class="ss">name: </span><span class="s2">"John"</span> <span class="p">}]</span>
<span class="nb">puts</span> <span class="n">api_response</span><span class="p">.</span><span class="nf">pluck</span><span class="p">(</span><span class="ss">:name</span><span class="p">)</span> 
<span class="c1"># Outputs: ["Zil", "John"]</span>
</code></pre></div></div>

<h2 id="summary">Summary</h2>

<p>ActiveSupport is basically the ultimate utility belt for Ruby.</p>

<p>When you learn how to use it outside of Rails, you realize that Rails isn’t just one big magic black box. It is a collection of really well-written, separate tools. Pulling <code class="language-plaintext highlighter-rouge">activesupport</code> into your plain Ruby scripts will save you from writing hundreds of lines of custom helper methods.</p>

<p>That’s pretty much it. Next time you write a web scraper or a background worker, don’t suffer with plain Ruby time calculations. Just require ActiveSupport.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="ruby" /><category term="rails" /><category term="activesupport" /><category term="tutorial" /><summary type="html"><![CDATA[Very often I find myself writing small, standalone Ruby scripts. Maybe it’s a web scraper, a small background worker, or a quick Sinatra API.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-04-How-To-Use-Rails-Magic-Methods-In-Plain-Ruby-Scripts/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-04-How-To-Use-Rails-Magic-Methods-In-Plain-Ruby-Scripts/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Ultimate Showdown: Rails 8 vs Phoenix LiveView</title><link href="https://norvilis.com/the-ultimate-showdown-rails-8-vs-phoenix-liveview/" rel="alternate" type="text/html" title="The Ultimate Showdown: Rails 8 vs Phoenix LiveView" /><published>2026-04-03T00:00:00+00:00</published><updated>2026-04-03T00:00:00+00:00</updated><id>https://norvilis.com/the-ultimate-showdown-rails-8-vs-phoenix-liveview</id><content type="html" xml:base="https://norvilis.com/the-ultimate-showdown-rails-8-vs-phoenix-liveview/"><![CDATA[<p>If you are a Ruby on Rails developer, you have definitely heard about <strong>Elixir</strong> and the <strong>Phoenix</strong> framework.</p>

<p>It is almost impossible to ignore. The creator of Elixir, José Valim, was a core contributor to Rails. He built Elixir because he loved Ruby’s beautiful syntax, but he wanted to fix Ruby’s biggest problem: handling high concurrency and real-time features.</p>

<p>Very often I see developers asking if Phoenix is the “new Rails” and if they should abandon Ruby to learn it. Here is my honest breakdown of how Ruby on Rails and Elixir Phoenix compare in 2026, and which one you should actually use for your next project.</p>

<h2 id="1-the-language-oop-vs-functional">1. The Language: OOP vs Functional</h2>

<p>The biggest difference between these two frameworks isn’t the frameworks themselves. It is the language they are written in.</p>

<p><strong>Ruby</strong> is strictly Object-Oriented. You create classes, you initialize objects, and those objects hold “state” (data that changes over time).
<strong>Elixir</strong> is a Functional programming language. There are no classes and no objects. Data is immutable. You pass data into a function, and it returns new data.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># ruby (Object-Oriented)</span>
<span class="n">user</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="ss">name: </span><span class="s2">"zil"</span><span class="p">)</span>
<span class="n">user</span><span class="p">.</span><span class="nf">capitalize_name!</span>
<span class="nb">puts</span> <span class="n">user</span><span class="p">.</span><span class="nf">name</span> <span class="c1"># Outputs: Zil</span>
</code></pre></div></div>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># elixir (Functional)</span>
<span class="n">user</span> <span class="o">=</span> <span class="p">%{</span><span class="ss">name:</span> <span class="s2">"zil"</span><span class="p">}</span>
<span class="c1"># We pass the user data through a pipe of functions</span>
<span class="n">updated_user</span> <span class="o">=</span> <span class="n">user</span> <span class="o">|&gt;</span> <span class="no">Map</span><span class="o">.</span><span class="n">update!</span><span class="p">(</span><span class="ss">:name</span><span class="p">,</span> <span class="o">&amp;</span><span class="no">String</span><span class="o">.</span><span class="n">capitalize</span><span class="o">/</span><span class="mi">1</span><span class="p">)</span>
<span class="no">IO</span><span class="o">.</span><span class="n">puts</span> <span class="n">updated_user</span><span class="o">.</span><span class="n">name</span> <span class="c1"># Outputs: Zil</span>
</code></pre></div></div>

<p>If you have only ever written Ruby or JavaScript, learning Elixir will literally rewire your brain. It takes time to get used to it.</p>

<h2 id="2-performance-and-concurrency-the-phoenix-win">2. Performance and Concurrency (The Phoenix Win)</h2>

<p>This is the main reason people switch to Phoenix.</p>

<p>Under the hood, Elixir runs on the <strong>Erlang VM (BEAM)</strong>. This technology was built decades ago by Ericsson to run telephone switches. It was designed to handle millions of phone calls at the exact same time without crashing.</p>

<p>If a process crashes in Elixir, it doesn’t take down your server. It just restarts that tiny process instantly.</p>

<p>Because of this, <strong>Phoenix</strong> can handle millions of active WebSocket connections on a single server. If you are building a real-time chat application, a live multiplayer game, or a stock trading platform, Phoenix will absolutely destroy Rails in performance and server costs.</p>

<h2 id="3-the-database-activerecord-vs-ecto">3. The Database: ActiveRecord vs Ecto</h2>

<p>When we use Rails, we use <strong>ActiveRecord</strong>. It is famous for being incredibly easy to use. It hides the SQL database from you.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># rails</span>
<span class="vi">@users</span> <span class="o">=</span> <span class="no">User</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">active: </span><span class="kp">true</span><span class="p">).</span><span class="nf">order</span><span class="p">(</span><span class="ss">created_at: :desc</span><span class="p">)</span>
</code></pre></div></div>

<p>In Phoenix, you use <strong>Ecto</strong>. Ecto is not an ORM (Object-Relational Mapper) because Elixir doesn’t have objects. It is a database wrapper that forces you to be very explicit about what you are doing.</p>

<div class="language-elixir highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># elixir</span>
<span class="n">query</span> <span class="o">=</span> <span class="n">from</span> <span class="n">u</span> <span class="ow">in</span> <span class="no">User</span><span class="p">,</span>
        <span class="ss">where:</span> <span class="n">u</span><span class="o">.</span><span class="n">active</span> <span class="o">==</span> <span class="no">true</span><span class="p">,</span>
        <span class="ss">order_by:</span> <span class="p">[</span><span class="ss">desc:</span> <span class="n">u</span><span class="o">.</span><span class="n">inserted_at</span><span class="p">]</span>

<span class="n">users</span> <span class="o">=</span> <span class="no">Repo</span><span class="o">.</span><span class="n">all</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>The Good:</strong> Ecto makes it very hard to accidentally write slow database queries. There is no “N+1” magic hiding behind the scenes.
<strong>The Bad:</strong> You have to write a lot more code. ActiveRecord is much faster for prototyping and building MVPs (Minimum Viable Products).</p>

<h2 id="4-frontend-magic-hotwire-vs-liveview">4. Frontend Magic: Hotwire vs LiveView</h2>

<p>Both frameworks realized that building separate React/Vue frontends is exhausting. They both created solutions to let you write interactive, SPA-like apps using only server-side code.</p>

<ul>
  <li><strong>Phoenix LiveView:</strong> This was the pioneer. When a user connects to a LiveView page, Phoenix opens a permanent WebSocket connection. The server holds the “state” of the page. When you click a button, the server calculates the change and pushes a tiny chunk of HTML over the socket to update the screen. Because Elixir processes are so lightweight, holding thousands of open websockets is easy.</li>
  <li><strong>Rails Hotwire:</strong> Rails answered with Hotwire. Instead of keeping a heavy, permanent stateful connection open for every user, Rails uses standard stateless HTTP requests (Turbo Drive/Frames) to fetch HTML, and only uses WebSockets (ActionCable / Solid Cable) when it needs to broadcast live updates.</li>
</ul>

<p>Both are amazing. LiveView is slightly more powerful for deeply complex real-time UI, but Hotwire is simpler to deploy and scale because it relies on standard HTTP caching.</p>

<h2 id="5-the-ecosystem-the-rails-win">5. The Ecosystem (The Rails Win)</h2>

<p>This is where Rails pulls ahead for solo developers.</p>

<p>If you want to add user authentication to Rails, you use <code class="language-plaintext highlighter-rouge">has_secure_password</code> or Devise. If you want to integrate Stripe, there is an official Stripe Ruby gem with thousands of StackOverflow answers. If you want background jobs, Rails 8 gives you Solid Queue out of the box.</p>

<p>Elixir’s package manager (Hex) is growing, but it is nowhere near the size of RubyGems. If you are building a standard SaaS app and you need to integrate with five different third-party APIs, you will probably have to write the API wrappers yourself in Elixir. In Ruby, you just type <code class="language-plaintext highlighter-rouge">bundle add ...</code>.</p>

<h2 id="summary-which-one-should-you-pick">Summary: Which one should you pick?</h2>

<p>The truth is, 95% of web applications do not need the insane concurrency power of Elixir.</p>

<ul>
  <li><strong>Choose Phoenix (Elixir)</strong> if your app’s main feature is real-time communication. If you are building the next Discord, WhatsApp, or a live betting platform, Phoenix is the best tool on the market. Period.</li>
  <li><strong>Choose Rails (Ruby)</strong> if you are building a SaaS, a marketplace, a blog, or a standard web application. The development speed, the massive ecosystem of gems, and the simplicity of ActiveRecord will help you launch your product weeks or months faster than if you used Elixir.</li>
</ul>

<p>That’s pretty much it. Both communities are fantastic, and learning functional programming with Elixir will make you a better Ruby developer anyway. But for getting a business off the ground quickly? I’m sticking with Rails.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="ruby" /><category term="elixir" /><category term="rails" /><category term="webdev" /><summary type="html"><![CDATA[If you are a Ruby on Rails developer, you have definitely heard about Elixir and the Phoenix framework.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-03-The-Ultimate-Showdown-Rails-8-Vs-Phoenix-Liveview/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-03-The-Ultimate-Showdown-Rails-8-Vs-Phoenix-Liveview/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Ditch Node.js: A Simple Guide to Rails Importmaps</title><link href="https://norvilis.com/ditch-node-js-a-simple-guide-to-rails-importmaps/" rel="alternate" type="text/html" title="Ditch Node.js: A Simple Guide to Rails Importmaps" /><published>2026-04-02T00:00:00+00:00</published><updated>2026-04-02T00:00:00+00:00</updated><id>https://norvilis.com/ditch-node-js-a-simple-guide-to-rails-importmaps</id><content type="html" xml:base="https://norvilis.com/ditch-node-js-a-simple-guide-to-rails-importmaps/"><![CDATA[<h1 id="how-importmaps-work-in-rails-and-why-you-dont-need-webpack">How Importmaps Work in Rails (And Why You Don’t Need Webpack)</h1>

<p>When I first started building modern Rails apps, managing JavaScript was always the most annoying part. We had Webpacker, <code class="language-plaintext highlighter-rouge">package.json</code>, and massive <code class="language-plaintext highlighter-rouge">node_modules</code> folders that took up gigabytes of space on my computer.</p>

<p>Very often I found myself tired of waiting for Webpack to compile my code just to see a small JavaScript change in the browser. It felt very heavy, especially for solo developers.</p>

<p>Then Rails introduced <strong>Importmaps</strong>. At first, I was very confused. How could I use libraries like Lodash, Chart.js, or Stimulus without running <code class="language-plaintext highlighter-rouge">npm install</code>?</p>

<p>Once I understood how it actually works under the hood, I realized it is a genius solution. Here is a simple explanation of how Importmaps work in Rails and how to use them.</p>

<h2 id="the-core-concept-a-dictionary-for-the-browser">The Core Concept: A Dictionary for the Browser</h2>

<p>Modern web browsers are actually very smart now. They natively understand ES Modules. This means if you write <code class="language-plaintext highlighter-rouge">import canvasConfetti from 'canvas-confetti'</code>, modern Chrome or Firefox knows what to do.</p>

<p>But there is one problem. The browser doesn’t know <strong>where</strong> to find that file on the internet.</p>

<p>An Importmap is basically just a dictionary that lives in your HTML <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code>. It tells the browser: <em>“Hey, if the Javascript code asks for ‘canvas-confetti’, please download it from this specific CDN URL.”</em></p>

<p>Here is how we set this up in Rails in 4 simple steps.</p>

<h2 id="step-1-pinning-a-library">STEP 1: Pinning a Library</h2>

<p>Instead of using <code class="language-plaintext highlighter-rouge">npm install</code> or <code class="language-plaintext highlighter-rouge">yarn add</code>, Rails gives us a simple terminal command to add a Javascript library to our app. We call this “pinning”.</p>

<p>Let’s say we want to add the popular <code class="language-plaintext highlighter-rouge">canvas-confetti</code> library. Go to your terminal and run:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>bin/importmap pin canvas-confetti
</code></pre></div></div>

<p>When you run this, Rails goes to a fast CDN (like <code class="language-plaintext highlighter-rouge">jspm.io</code>), finds the exact URL for that library, and saves it to your configuration.</p>

<h2 id="step-2-the-configuration-file">STEP 2: The Configuration File</h2>

<p>If you open the file <code class="language-plaintext highlighter-rouge">config/importmap.rb</code>, you will see what Rails actually did in the background.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># config/importmap.rb</span>

<span class="c1"># These are your local files</span>
<span class="n">pin</span> <span class="s2">"application"</span>
<span class="n">pin</span> <span class="s2">"@hotwired/turbo-rails"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"turbo.min.js"</span>
<span class="n">pin</span> <span class="s2">"@hotwired/stimulus"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"stimulus.min.js"</span>
<span class="n">pin</span> <span class="s2">"@hotwired/stimulus-loading"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"stimulus-loading.js"</span>

<span class="c1"># This is the new library we just pinned!</span>
<span class="n">pin</span> <span class="s2">"canvas-confetti"</span><span class="p">,</span> <span class="ss">to: </span><span class="s2">"https://ga.jspm.io/npm:canvas-confetti@1.9.2/dist/confetti.module.mjs"</span>
</code></pre></div></div>

<p>This file is just a list of names and URLs. You can even manually edit this file to point to different CDNs if you want.</p>

<h2 id="step-3-using-it-in-your-code">STEP 3: Using it in your Code</h2>

<p>Now that the library is “pinned”, we can use it anywhere in our JavaScript files just like we would in a React or Node.js app.</p>

<p>Open your <code class="language-plaintext highlighter-rouge">app/javascript/application.js</code> (or a Stimulus controller) and simply import it:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/application.js</span>

<span class="k">import</span> <span class="nx">confetti</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">canvas-confetti</span><span class="dl">"</span><span class="p">;</span>

<span class="c1">// Let's trigger it!</span>
<span class="nx">confetti</span><span class="p">({</span>
  <span class="na">particleCount</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span>
  <span class="na">spread</span><span class="p">:</span> <span class="mi">70</span><span class="p">,</span>
  <span class="na">origin</span><span class="p">:</span> <span class="p">{</span> <span class="na">y</span><span class="p">:</span> <span class="mf">0.6</span> <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Because we pinned the name <code class="language-plaintext highlighter-rouge">"canvas-confetti"</code>, the browser knows exactly what we are talking about.</p>

<h2 id="step-4-the-magic-in-the-html">STEP 4: The Magic in the HTML</h2>

<p>So how does the browser actually read this dictionary?</p>

<p>In your <code class="language-plaintext highlighter-rouge">app/views/layouts/application.html.erb</code>, you should have this standard Rails helper in the <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> section:</p>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;%=</span> <span class="n">javascript_importmap_tags</span> <span class="cp">%&gt;</span>
</code></pre></div></div>

<p>When a user visits your website, Rails turns that helper into a literal JSON script tag. If you inspect your page source in the browser, it looks like this:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"importmap"</span><span class="nt">&gt;</span>
  <span class="p">{</span>
    <span class="dl">"</span><span class="s2">imports</span><span class="dl">"</span><span class="p">:</span> <span class="p">{</span>
      <span class="dl">"</span><span class="s2">application</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">/assets/application-xyz123.js</span><span class="dl">"</span><span class="p">,</span>
      <span class="dl">"</span><span class="s2">canvas-confetti</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">https://ga.jspm.io/npm:canvas-confetti@1.9.2/dist/confetti.module.mjs</span><span class="dl">"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>When the browser hits your <code class="language-plaintext highlighter-rouge">application.js</code> file and sees <code class="language-plaintext highlighter-rouge">import confetti from "canvas-confetti"</code>, it looks at this JSON map, finds the URL, downloads the file directly from the CDN, and executes it.</p>

<h2 id="why-i-like-this-approach">Why I like this approach?</h2>

<p>There are a few massive reasons why I prefer Importmaps over the old Webpack/Node.js way:</p>

<ol>
  <li><strong>No Build Step:</strong> You don’t have to wait for your JavaScript to compile. You hit “Save” in your editor, refresh the browser, and the changes are there instantly.</li>
  <li><strong>No <code class="language-plaintext highlighter-rouge">node_modules</code>:</strong> Your Rails project stays incredibly small and clean. You don’t have a 500MB folder of Javascript dependencies sitting on your hard drive.</li>
  <li><strong>Fast for Users:</strong> Because libraries are downloaded from public CDNs, there is a very high chance the user’s browser already has that library cached from visiting another website.</li>
</ol>

<p>Importmaps remove a huge layer of complexity from frontend development. If you are building a standard Rails app with Hotwire and Stimulus, you really don’t need a JavaScript bundler anymore.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="ruby" /><category term="javascript" /><category term="webdev" /><summary type="html"><![CDATA[How Importmaps Work in Rails (And Why You Don’t Need Webpack)]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-02-Ditch-Node-Js-A-Simple-Guide-To-Rails-Importmaps/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-02-Ditch-Node-Js-A-Simple-Guide-To-Rails-Importmaps/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Hotwire vs Inertia.js: Which One Should You Use in Rails?</title><link href="https://norvilis.com/hotwire-vs-inertia-js-which-one-should-you-use-in-rails/" rel="alternate" type="text/html" title="Hotwire vs Inertia.js: Which One Should You Use in Rails?" /><published>2026-04-01T00:00:00+00:00</published><updated>2026-04-01T00:00:00+00:00</updated><id>https://norvilis.com/hotwire-vs-inertia-js-which-one-should-you-use-in-rails</id><content type="html" xml:base="https://norvilis.com/hotwire-vs-inertia-js-which-one-should-you-use-in-rails/"><![CDATA[<p>For a long time, if you wanted to build a fast, modern web application without page reloads, you had to build two completely separate apps. You had to build a Rails API for the backend, and a React or Vue Single Page Application (SPA) for the frontend.</p>

<p>Very often I found myself tired of this approach. Managing JSON responses, CORS issues, and duplicating routing logic is just annoying.</p>

<p>Thankfully, the industry realized this was too complex for solo developers. Two massive solutions appeared to fix this: <strong>Hotwire</strong> (created by the Rails team) and <strong>Inertia.js</strong> (popularized by the Laravel community).</p>

<p>Both of these tools let you build modern “SPA-like” apps without building an API. But they work in very different ways. Here is my breakdown of how they compare, and why I personally favor Hotwire.</p>

<h2 id="the-inertiajs-approach-the-modern-monolith">The Inertia.js Approach (The Modern Monolith)</h2>

<p>Inertia.js acts as a glue between your backend (Rails/Laravel) and your frontend (React/Vue/Svelte).</p>

<p>With Inertia, you still use Rails for your routes and controllers. But instead of rendering an HTML file (<code class="language-plaintext highlighter-rouge">.html.erb</code>), your controller sends data to a JavaScript component.</p>

<p>Here is how an Inertia controller looks in Rails:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/dashboards_controller.rb</span>
<span class="k">class</span> <span class="nc">DashboardsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">show</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="n">current_user</span>
    <span class="c1"># Instead of rendering ERB, we render a React/Vue component</span>
    <span class="n">render</span> <span class="ss">inertia: </span><span class="s1">'Dashboard'</span><span class="p">,</span> <span class="ss">props: </span><span class="p">{</span>
      <span class="ss">user: </span><span class="vi">@user</span><span class="p">.</span><span class="nf">as_json</span>
    <span class="p">}</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<p>And your frontend is a standard React file:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// app/javascript/Pages/Dashboard.jsx</span>
<span class="k">import</span> <span class="nx">React</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">react</span><span class="dl">'</span><span class="p">;</span>

<span class="k">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="nx">Dashboard</span><span class="p">({</span> <span class="nx">user</span> <span class="p">})</span> <span class="p">{</span>
  <span class="k">return</span> <span class="p">(</span>
    <span class="o">&lt;</span><span class="nx">div</span><span class="o">&gt;</span>
      <span class="o">&lt;</span><span class="nx">h1</span><span class="o">&gt;</span><span class="nx">Welcome</span> <span class="nx">back</span><span class="p">,</span> <span class="p">{</span><span class="nx">user</span><span class="p">.</span><span class="nx">name</span><span class="p">}</span><span class="o">!&lt;</span><span class="sr">/h1</span><span class="err">&gt;
</span>    <span class="o">&lt;</span><span class="sr">/div</span><span class="err">&gt;
</span>  <span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>The Good:</strong> You get to use React or Vue. If you love the React ecosystem and want to use components like Material UI or Tailwind UI React, this is amazing.
<strong>The Bad:</strong> You are sending JSON over the wire. Your browser still has to download the heavy JavaScript framework, parse the JSON, and build the HTML on the user’s device.</p>

<h2 id="the-hotwire-approach-html-over-the-wire">The Hotwire Approach (HTML Over The Wire)</h2>

<p>Hotwire takes the opposite approach. It says: “JavaScript is too heavy. Let the server do the work.”</p>

<p>With Hotwire, you just write standard Rails controllers and standard ERB views.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># app/controllers/dashboards_controller.rb</span>
<span class="k">class</span> <span class="nc">DashboardsController</span> <span class="o">&lt;</span> <span class="no">ApplicationController</span>
  <span class="k">def</span> <span class="nf">show</span>
    <span class="vi">@user</span> <span class="o">=</span> <span class="n">current_user</span>
    <span class="c1"># Rails automatically renders show.html.erb</span>
  <span class="k">end</span>
<span class="k">end</span>
</code></pre></div></div>

<div class="language-erb highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- app/views/dashboards/show.html.erb --&gt;</span>
<span class="nt">&lt;div&gt;</span>
  <span class="nt">&lt;h1&gt;</span>Welcome back, <span class="cp">&lt;%=</span> <span class="vi">@user</span><span class="p">.</span><span class="nf">name</span> <span class="cp">%&gt;</span>!<span class="nt">&lt;/h1&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>When a user clicks a link, Hotwire intercepts the click, fetches the new HTML from the server in the background, and instantly swaps out the <code class="language-plaintext highlighter-rouge">&lt;body&gt;</code> of the page.</p>

<h2 id="why-i-favor-hotwire">Why I Favor Hotwire</h2>

<p>Both tools are great, but for a solo developer or a small team using Rails, I think Hotwire is the clear winner. Here is why.</p>

<h3 id="reason-1-no-context-switching">REASON 1: No Context Switching</h3>
<p>If you use Inertia, you have to write Ruby in your controllers, and then switch your brain to write JavaScript/React for your views. You have to remember two different syntaxes, two different ways to loop through arrays, and two different ways to format dates. 
With Hotwire, you stay in Ruby and HTML almost 100% of the time.</p>

<h3 id="reason-2-the-build-step-nodejs">REASON 2: The Build Step (Node.js)</h3>
<p>To use Inertia, you must have Node.js, Webpack, or Vite running to compile your React/Vue code. This means dealing with <code class="language-plaintext highlighter-rouge">package.json</code> and <code class="language-plaintext highlighter-rouge">node_modules</code>. 
With Rails 7 and Rails 8, Hotwire uses Importmaps. You literally do not need Node.js installed on your computer. You write plain code, and it just works in the browser. It makes your development environment much simpler.</p>

<h3 id="reason-3-state-management">REASON 3: State Management</h3>
<p>In Inertia (or any React app), you have to worry about “State”. If you update a user’s name, you have to write Javascript to update that variable on the screen. 
In Hotwire, the database is your only state. If something changes, you just tell the server to render that small piece of HTML again using a <code class="language-plaintext highlighter-rouge">Turbo Stream</code>. It deletes an entire category of bugs.</p>

<h3 id="reason-4-it-is-the-rails-default">REASON 4: It is the Rails Default</h3>
<p>If you use Inertia in Rails, you are fighting against the current. The <code class="language-plaintext highlighter-rouge">inertia_rails</code> gem is good, but you miss out on built-in Rails features like standard form helpers and ActionCable broadcasting. 
Hotwire is built directly into Rails by the creators of Rails. Everything works perfectly out of the box.</p>

<h2 id="summary">Summary</h2>

<ul>
  <li>Use <strong>Inertia.js</strong> if you are moving from a dedicated React/Vue frontend and you already have a massive library of React components you want to reuse, but you are tired of writing API endpoints.</li>
  <li>Use <strong>Hotwire</strong> if you want to build things incredibly fast, keep your codebase simple, and stay inside the Ruby ecosystem.</li>
</ul>

<p>That’s pretty much it. For me, removing the Javascript build step and keeping all my logic in Ruby makes Hotwire the ultimate productivity tool.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="rails" /><category term="hotwire" /><category term="inertia" /><category term="webdev" /><summary type="html"><![CDATA[For a long time, if you wanted to build a fast, modern web application without page reloads, you had to build two completely separate apps. You had to build a Rails API for the backend, and a React or Vue Single Page Application (SPA) for the frontend.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-04-01-Hotwire-Vs-Inertia-Js-Which-One-Should-You-Use-In-Rails/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-04-01-Hotwire-Vs-Inertia-Js-Which-One-Should-You-Use-In-Rails/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The Secret to Fast Web Scraping: Finding Internal JSON APIs</title><link href="https://norvilis.com/the-secret-to-fast-web-scraping-finding-internal-json-apis/" rel="alternate" type="text/html" title="The Secret to Fast Web Scraping: Finding Internal JSON APIs" /><published>2026-03-31T00:00:00+00:00</published><updated>2026-03-31T00:00:00+00:00</updated><id>https://norvilis.com/the-secret-to-fast-web-scraping-finding-internal-json-apis</id><content type="html" xml:base="https://norvilis.com/the-secret-to-fast-web-scraping-finding-internal-json-apis/"><![CDATA[<p>When I first started web scraping, my workflow was always the same. I would use <code class="language-plaintext highlighter-rouge">Nokogiri</code> to download the HTML page, and then I would spend hours writing crazy CSS selectors to extract the text I needed.</p>

<p>If the website was built with React or Vue and the data loaded dynamically, I would boot up a heavy headless browser like Selenium or Playwright just to wait for the page to render.</p>

<p>Very often I find myself frustrated because websites change their CSS classes all the time, breaking my scraper. But recently, I changed my approach completely.</p>

<p>Modern websites are basically just empty shells that fetch data from <strong>internal, hidden APIs</strong>. If you can find that API, you can skip the HTML completely and just download perfectly structured JSON data. It is 100x faster and much more reliable.</p>

<p>Here is how to find and scrape hidden APIs in 4 easy steps.</p>

<h2 id="step-1-the-detective-work-network-tab">STEP 1: The Detective Work (Network Tab)</h2>

<p>You don’t need any special hacking tools for this. Just use your browser. Let’s say you want to scrape a list of products from an e-commerce store.</p>

<ol>
  <li>Open the website in Google Chrome.</li>
  <li>Right-click anywhere on the page and select <strong>Inspect</strong> to open DevTools.</li>
  <li>Go to the <strong>Network</strong> tab.</li>
  <li>Click the filter button that says <strong>Fetch/XHR</strong>. (This hides all the images, CSS, and fonts, showing only data requests).</li>
  <li>Now, refresh the page or scroll down to load more products.</li>
</ol>

<p>You will see a list of requests appear. Click on them one by one and look at the <strong>Preview</strong> or <strong>Response</strong> tab. You are looking for the one that returns a clean JSON object containing the product data.</p>

<h2 id="step-2-copy-as-curl">STEP 2: Copy as cURL</h2>

<p>Once you find the correct API request, you can’t just copy the URL and paste it into your Ruby script. Internal APIs usually require specific headers to work, like a <code class="language-plaintext highlighter-rouge">User-Agent</code>, an <code class="language-plaintext highlighter-rouge">Accept</code> header, or an authorization token.</p>

<p>Chrome makes it very easy to grab all of this.</p>

<ol>
  <li>Right-click the successful request in the Network tab.</li>
  <li>Go to <strong>Copy</strong> -&gt; <strong>Copy as cURL</strong>.</li>
</ol>

<p>Now you have the exact command, including all the secret headers the browser used, copied to your clipboard.</p>

<h2 id="step-3-convert-to-ruby-code">STEP 3: Convert to Ruby Code</h2>

<p>Now we need to translate that cURL command into a Ruby script. 
You can do this manually, but the fastest way is to go to a free site like <a href="https://curlconverter.com/ruby/">curlconverter.com</a> and paste your cURL command. It will instantly generate the Ruby code for you.</p>

<p>Here is what a typical request looks like using the <code class="language-plaintext highlighter-rouge">http</code> gem:</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># scraper.rb</span>
<span class="nb">require</span> <span class="s1">'http'</span>
<span class="nb">require</span> <span class="s1">'json'</span>

<span class="n">url</span> <span class="o">=</span> <span class="s2">"https://api.example-store.com/v1/products"</span>

<span class="c1"># We pass the headers we copied from Chrome</span>
<span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s2">"User-Agent"</span> <span class="o">=&gt;</span> <span class="s2">"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36..."</span><span class="p">,</span>
  <span class="s2">"Accept"</span> <span class="o">=&gt;</span> <span class="s2">"application/json"</span><span class="p">,</span>
  <span class="s2">"Referer"</span> <span class="o">=&gt;</span> <span class="s2">"https://example-store.com/category/shoes"</span>
<span class="p">}</span>

<span class="c1"># Make the request to the hidden API</span>
<span class="n">response</span> <span class="o">=</span> <span class="no">HTTP</span><span class="p">.</span><span class="nf">headers</span><span class="p">(</span><span class="n">headers</span><span class="p">).</span><span class="nf">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>

<span class="k">if</span> <span class="n">response</span><span class="p">.</span><span class="nf">status</span><span class="p">.</span><span class="nf">success?</span>
  <span class="c1"># Parse the JSON response</span>
  <span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
  
  <span class="c1"># Loop through the data easily!</span>
  <span class="n">data</span><span class="p">[</span><span class="s2">"items"</span><span class="p">].</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">product</span><span class="o">|</span>
    <span class="nb">puts</span> <span class="s2">"Name: </span><span class="si">#{</span><span class="n">product</span><span class="p">[</span><span class="s1">'name'</span><span class="p">]</span><span class="si">}</span><span class="s2"> - Price: $</span><span class="si">#{</span><span class="n">product</span><span class="p">[</span><span class="s1">'price'</span><span class="p">]</span><span class="si">}</span><span class="s2">"</span>
  <span class="k">end</span>
<span class="k">else</span>
  <span class="nb">puts</span> <span class="s2">"Failed to fetch data."</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="step-4-pagination-the-infinite-scroll">STEP 4: Pagination (The Infinite Scroll)</h2>

<p>Usually, the first API request only gives you 20 or 50 items. If you want to scrape the whole category, you need to figure out how the API handles pagination.</p>

<p>Go back to your Chrome Network tab and look at the <strong>Payload</strong> (or the URL parameters) of the request.</p>

<p>You will usually see something like this:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">?page=1</code></li>
  <li><code class="language-plaintext highlighter-rouge">?offset=20</code></li>
  <li><code class="language-plaintext highlighter-rouge">?cursor=abc123xyz</code></li>
</ul>

<p>To scrape all the pages, you just wrap your Ruby request in a simple loop, incrementing the page number or cursor each time until the API returns an empty array.</p>

<div class="language-ruby highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Example of a simple pagination loop</span>
<span class="n">page</span> <span class="o">=</span> <span class="mi">1</span>
<span class="kp">loop</span> <span class="k">do</span>
  <span class="n">response</span> <span class="o">=</span> <span class="no">HTTP</span><span class="p">.</span><span class="nf">headers</span><span class="p">(</span><span class="n">headers</span><span class="p">).</span><span class="nf">get</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">url</span><span class="si">}</span><span class="s2">?page=</span><span class="si">#{</span><span class="n">page</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="n">data</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
  
  <span class="k">break</span> <span class="k">if</span> <span class="n">data</span><span class="p">[</span><span class="s2">"items"</span><span class="p">].</span><span class="nf">empty?</span> <span class="c1"># Stop when no more products</span>
  
  <span class="c1"># Process items here...</span>
  
  <span class="n">page</span> <span class="o">+=</span> <span class="mi">1</span>
  <span class="nb">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># Be nice to their server!</span>
<span class="k">end</span>
</code></pre></div></div>

<h2 id="why-i-like-this-approach">Why I like this approach?</h2>

<p>There are a few reasons why I prefer scraping hidden APIs over parsing HTML:</p>

<ol>
  <li><strong>Speed:</strong> You are not downloading megabytes of images, fonts, and javascript files. You are just downloading tiny text files. It is incredibly fast.</li>
  <li><strong>Stability:</strong> Frontend developers change HTML structure and CSS classes all the time to update the design. They rarely change the internal API structure, so your scraper won’t break as often.</li>
  <li><strong>Hidden Data:</strong> Very often, the JSON API returns <em>more</em> data than the website actually displays on the screen. You might find exact stock counts, internal product IDs, or hidden categories that are super useful for your project.</li>
</ol>

<p>That’s pretty much it. Next time you need to scrape a modern website, don’t reach for Nokogiri or Selenium right away. Spend 5 minutes in the Network tab first. It might save you hours of work.</p>]]></content><author><name>Zil Norvilis</name></author><category term="selfnote" /><category term="ruby" /><category term="scraping" /><category term="webdev" /><category term="tutorial" /><summary type="html"><![CDATA[When I first started web scraping, my workflow was always the same. I would use Nokogiri to download the HTML page, and then I would spend hours writing crazy CSS selectors to extract the text I needed.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://norvilis.com/assets/images/2026-03-31-The-Secret-To-Fast-Web-Scraping-Finding-Internal-Json-Apis/feature.webp" /><media:content medium="image" url="https://norvilis.com/assets/images/2026-03-31-The-Secret-To-Fast-Web-Scraping-Finding-Internal-Json-Apis/feature.webp" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>