<?xml version="1.0" encoding="utf-8" standalone="yes"?><feed xmlns="http://www.w3.org/2005/Atom">
  <title></title>
  <subtitle></subtitle>
  <id>https://www.endpointdev.com/blog/tags/rails/</id>
  <link href="https://www.endpointdev.com/blog/tags/rails/"/>
  <link href="https://www.endpointdev.com/blog/tags/rails/" rel="self"/>
  <updated>2026-06-08T00:00:00+00:00</updated>
  <author>
    <name>End Point Dev</name>
  </author>
  
    <entry>
      <title>Building a web app using Rails 8 and Vue 3 with Vite</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/06/building-a-web-app-using-rails-8-and-vue-3-with-vite/"/>
      <id>https://www.endpointdev.com/blog/2026/06/building-a-web-app-using-rails-8-and-vue-3-with-vite/</id>
      <published>2026-06-08T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/06/building-a-web-app-using-rails-8-and-vue-3-with-vite/cover.webp&#34; alt=&#34;New York City skyline at dusk, with warm orange and pink hues across the sky and a foreground building topped with a glass pyramid structure.&#34;&gt;&lt;br&gt;
Photo by Jonathan Perlin, 2022.&lt;/p&gt;
&lt;p&gt;When it comes to frontend development, &lt;a href=&#34;https://rubyonrails.org/&#34;&gt;Rails 8&lt;/a&gt; offers many great options.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s of course &lt;a href=&#34;https://hotwired.dev/&#34;&gt;Hotwire&lt;/a&gt;: the new little-to-no-JavaScript frontend framework that leverages server-side rendering, &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API&#34;&gt;WebSockets&lt;/a&gt;, and &lt;a href=&#34;https://stimulus.hotwired.dev/&#34;&gt;Stimulus&lt;/a&gt; to deliver rich, &lt;a href=&#34;https://en.wikipedia.org/wiki/Single-page_application&#34;&gt;SPA&lt;/a&gt;-feeling web apps.&lt;/p&gt;
&lt;p&gt;For serving static assets to web browsers, including JavaScript, Rails has the &lt;a href=&#34;https://guides.rubyonrails.org/asset_pipeline.html&#34;&gt;Asset Pipeline&lt;/a&gt;, which helps with fingerprinting, caching, and exposing assets.&lt;/p&gt;
&lt;p&gt;By default, the Rails Asset Pipeline is configured to use &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap&#34;&gt;Import Maps&lt;/a&gt; (with the &lt;a href=&#34;https://github.com/rails/importmap-rails&#34;&gt;&lt;code&gt;importmap-rails&lt;/code&gt;&lt;/a&gt; gem), which allows easy delivery and loading of &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules&#34;&gt;JavaScript as modules&lt;/a&gt;, without the need for bundling and/or transpiling. That means that your development environment and build pipeline do not need &lt;a href=&#34;https://nodejs.org&#34;&gt;Node.js&lt;/a&gt;, &lt;a href=&#34;https://yarnpkg.com/&#34;&gt;Yarn&lt;/a&gt;, or any other package manager or bundler. Rails takes care of everything using standard HTML features supported by all major browsers.&lt;/p&gt;
&lt;p&gt;Hotwire works beautifully with this setup.&lt;/p&gt;
&lt;p&gt;Now, if you need bundling and transpiling, the Rails Asset Pipeline can be configured to do so too. Instead of &lt;code&gt;importmap-rails&lt;/code&gt;, you&amp;rsquo;d be installing the &lt;a href=&#34;https://github.com/rails/jsbundling-rails&#34;&gt;&lt;code&gt;jsbundling-rails&lt;/code&gt;&lt;/a&gt; gem. This way you can use bundlers like &lt;a href=&#34;https://bun.com/&#34;&gt;Bun&lt;/a&gt;, &lt;a href=&#34;https://esbuild.github.io/&#34;&gt;esbuild&lt;/a&gt;, &lt;a href=&#34;https://rollupjs.org/&#34;&gt;Rollup.js&lt;/a&gt; and &lt;a href=&#34;https://webpack.js.org/&#34;&gt;Webpack&lt;/a&gt; to process your JavaScript source code and emit bundles ready for the browser to run. CSS preprocessors like &lt;a href=&#34;https://postcss.org/&#34;&gt;PostCSS&lt;/a&gt; and &lt;a href=&#34;https://sass-lang.com/dart-sass/&#34;&gt;Dart Sass&lt;/a&gt; are also supported with the &lt;a href=&#34;https://github.com/rails/cssbundling-rails&#34;&gt;&lt;code&gt;cssbundling-rails&lt;/code&gt;&lt;/a&gt; gem. In particular, this style is necessary when you need to integrate frontend frameworks like &lt;a href=&#34;https://react.dev/&#34;&gt;React&lt;/a&gt; or &lt;a href=&#34;https://svelte.dev/&#34;&gt;Svelte&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This used to be the case for &lt;a href=&#34;https://vuejs.org/&#34;&gt;Vue&lt;/a&gt; as well. However, the story is a bit different nowadays. In order to set up a Vue frontend for a Rails 8 app, it&amp;rsquo;s necessary to use &lt;a href=&#34;https://vite.dev/&#34;&gt;Vite&lt;/a&gt;. The main reason is that with the advent of Vue 3, a lot of the ecosystem around Vue &lt;a href=&#34;https://v3-migration.vuejs.org/recommendations&#34;&gt;changed&lt;/a&gt;. The necessary plugins that implement support for Vue on bundlers like esbuild and Webpack have fallen behind. Vite, which is maintained by the developers of Vue, is the new recommended way to build and deliver modern Vue apps. Thankfully, there exists the &lt;a href=&#34;https://vite-ruby.netlify.app/guide/introduction.html&#34;&gt;&lt;code&gt;vite_rails&lt;/code&gt;&lt;/a&gt; gem, which does the heavy lifting for us when it comes to integrating Vue with Rails.&lt;/p&gt;
&lt;p&gt;In this article, we will see a recipe for how to set up a Rails 8 app with Vue via Vite.&lt;/p&gt;
&lt;h3 id=&#34;the-development-environment&#34;&gt;The development environment&lt;/h3&gt;
&lt;p&gt;In order to build a Rails app, you of course need &lt;a href=&#34;https://www.ruby-lang.org/en/&#34;&gt;Ruby&lt;/a&gt; and the Rails gem. For the frontend part, you need Node.js and the package manager of your choice. We&amp;rsquo;ll just use &lt;a href=&#34;https://www.npmjs.com/&#34;&gt;npm&lt;/a&gt; here, but Yarn and &lt;a href=&#34;https://pnpm.io/&#34;&gt;pnpm&lt;/a&gt; work just as well.&lt;/p&gt;
&lt;p&gt;For development, I like to use &lt;a href=&#34;https://containers.dev/&#34;&gt;Development Containers&lt;/a&gt; with &lt;a href=&#34;https://code.visualstudio.com/&#34;&gt;Visual Studio Code&lt;/a&gt;. With this, you can get a &lt;a href=&#34;https://www.docker.com/&#34;&gt;Docker&lt;/a&gt; container that&amp;rsquo;s optimized for development thathat allows you to hit the ground running.&lt;/p&gt;
&lt;p&gt;In any case, if you want to follow along, make sure you have all these installed in your environment:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ ruby -v
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +PRISM [x86_64-linux]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ rails -v
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Rails 8.1.3
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ node -v
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;v24.15.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ npm -v
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;11.14.1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The code is also &lt;a href=&#34;https://github.com/megakevin/end-point-blog-rails8-vue3-vite&#34;&gt;on GitHub&lt;/a&gt;, including &lt;a href=&#34;https://github.com/megakevin/end-point-blog-rails8-vue3-vite/tree/main/.devcontainer&#34;&gt;the configuration of the devcontainer&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;creating-a-new-rails-8-project&#34;&gt;Creating a new Rails 8 project&lt;/h3&gt;
&lt;p&gt;To create the Rails project, we use a conventional CLI command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rails new . -n rails8-vue3-vite -d postgresql --skip-hotwire --skip-javascript&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, the interesting options are &lt;code&gt;--skip-hotwire&lt;/code&gt; and &lt;code&gt;--skip-javascript&lt;/code&gt;. We will be using Vue, so we tell Rails not to include support for Hotwire. Also, we will use Vite, which has its own conventions for handling JavaScript, so we use the &lt;code&gt;--skip-javascript&lt;/code&gt; so that Rails doesn&amp;rsquo;t create its default JavaScript files.&lt;/p&gt;
&lt;p&gt;We should also create a page that we can navigate to:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bundle &lt;span style=&#34;color:#038&#34;&gt;exec&lt;/span&gt; rails g controller home index&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After that, you should be able to run the app and navigate to the page we just created:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails &lt;span style=&#34;color:#369&#34;&gt;server&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;=&amp;gt; Booting &lt;span style=&#34;color:#369&#34;&gt;Puma&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;=&amp;gt; Rails 8.1.3 application starting in &lt;span style=&#34;color:#369&#34;&gt;development&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;=&amp;gt; Run &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;`&lt;/span&gt;bin/rails server --help&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;`&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; more startup options
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Puma starting in single mode...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* Puma version: 8.0.1 (&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Into the Arena&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* Ruby version: ruby 4.0.4 (2026-05-12 revision b89eb1bcbf) +PRISM [x86_64-linux]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;*  Min threads: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;*  Max threads: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;*  Environment: development
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;*          PID: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;45415&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* Listening on http://127.0.0.1:3000
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;* Listening on http://[::1]:3000
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Use Ctrl-C to stop&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&#34;/blog/2026/06/building-a-web-app-using-rails-8-and-vue-3-with-vite/home-page.png&#34; alt=&#34;The home page&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;setting-up-vite&#34;&gt;Setting up Vite&lt;/h3&gt;
&lt;p&gt;As I mentioned before, the &lt;code&gt;vite_rails&lt;/code&gt; gem comes in really handy for integrating Vite with Rails. We install it with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bundle add vite_rails
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bundle &lt;span style=&#34;color:#038&#34;&gt;exec&lt;/span&gt; vite install&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that, a few notable things should have happened in the project:&lt;/p&gt;
&lt;p&gt;1: The gem should&amp;rsquo;ve been added to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;vite_rails&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;2: A &lt;code&gt;package.json&lt;/code&gt; file should&amp;rsquo;ve been created with the necessary dependencies for Vite:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;private&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;module&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;devDependencies&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;vite&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;^8.0.13&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;vite-plugin-ruby&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;^5.2.2&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;3: A pair of Vite config files should&amp;rsquo;ve been created, which configure Vite&amp;rsquo;s bundling process and its development and test servers:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ts&#34; data-lang=&#34;ts&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// vite.config.ts
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { defineConfig } &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;vite&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; RubyPlugin &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;vite-plugin-ruby&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;default&lt;/span&gt; defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  plugins: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    RubyPlugin(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// config/vite.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;all&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;sourceCodeDir&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;app/frontend&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;watchAdditionalPaths&amp;#34;&lt;/span&gt;: []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;development&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;autoBuild&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;skipProxy&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;publicOutputDir&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;vite-dev&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;port&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3036&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;test&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;autoBuild&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;publicOutputDir&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;vite-test&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;port&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3037&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;4: An &lt;code&gt;app/frontend/entrypoints/application.js&lt;/code&gt; should&amp;rsquo;ve been created with some &lt;code&gt;console.log&lt;/code&gt; calls that are useful for validating that things are working. This is called an &amp;ldquo;&lt;a href=&#34;https://vite-ruby.netlify.app/guide/development.html#entrypoints-%E2%A4%B5%EF%B8%8F&#34;&gt;entry point&lt;/a&gt;&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;5: The following lines should&amp;rsquo;ve been added to your &lt;code&gt;app/views/layouts/application.html.erb&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= vite_client_tag %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= vite_javascript_tag &amp;#39;application&amp;#39; %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;vite_client_tag&lt;/code&gt; enables &lt;a href=&#34;https://vite.dev/guide/features#hot-module-replacement&#34;&gt;Hot Module Replacement&lt;/a&gt;, and &lt;code&gt;vite_javascript_tag &#39;application&#39;&lt;/code&gt; includes the bundle that Vite produces as a result of bundling &lt;code&gt;application.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;At this point, you can run the Vite development server and the Rails server in separate terminals:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/vite dev
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  VITE v8.0.13  ready in &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;146&lt;/span&gt; ms
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ➜  Local:   http://localhost:3036/vite-dev/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ➜  press h + enter to show help&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails server
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can now navigate to &lt;code&gt;http://127.0.0.1:3000/home/index&lt;/code&gt; and see this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/06/building-a-web-app-using-rails-8-and-vue-3-with-vite/home-page-with-vite.png&#34; alt=&#34;Home page with Vite&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you&amp;rsquo;re using devcontainers, you may have to specify &lt;code&gt;127.0.0.1&lt;/code&gt; as the &lt;code&gt;host&lt;/code&gt; in the Vite dev server configuration in order to allow your browser to connect to it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;all&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;sourceCodeDir&amp;#34;: &amp;#34;app/frontend&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;watchAdditionalPaths&amp;#34;: []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;development&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;autoBuild&amp;#34;: true,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;skipProxy&amp;#34;: true,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;publicOutputDir&amp;#34;: &amp;#34;vite-dev&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    &amp;#34;host&amp;#34;: &amp;#34;127.0.0.1&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;    &amp;#34;port&amp;#34;: 3036
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;test&amp;#34;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;autoBuild&amp;#34;: true,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;#34;publicOutputDir&amp;#34;: &amp;#34;vite-test&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    &amp;#34;host&amp;#34;: &amp;#34;127.0.0.1&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;    &amp;#34;port&amp;#34;: 3037
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;installing-vue&#34;&gt;Installing Vue&lt;/h3&gt;
&lt;p&gt;Finally, we can install Vue and the plugin that allows Vite to process &lt;code&gt;*.vue&lt;/code&gt; files:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install vue
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;npm install @vitejs/plugin-vue&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then wire it up in the &lt;code&gt;vite.config.ts&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;import { defineConfig } from &amp;#39;vite&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;import RubyPlugin from &amp;#39;vite-plugin-ruby&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+import vue from &amp;#39;@vitejs/plugin-vue&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;export default defineConfig({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  plugins: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    RubyPlugin(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    vue()
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;  ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this, we&amp;rsquo;re all set. We can start developing Vue components. Here&amp;rsquo;s a quick example.&lt;/p&gt;
&lt;p&gt;We can create a &lt;code&gt;app/frontend/components/App.vue&lt;/code&gt; file with these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;default&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  data() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      message: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Hello Rails, Vue and Vite!&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      count: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;template&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt;&amp;gt;{{ message }}&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;button&lt;/span&gt; &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;@&lt;/span&gt;&lt;span style=&#34;color:#369&#34;&gt;click&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;count++&amp;#34;&lt;/span&gt;&amp;gt;Count is: {{ count }}&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;button&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;template&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;style&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;scoped&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;background&lt;/span&gt;: &lt;span style=&#34;color:#038&#34;&gt;linear-gradient&lt;/span&gt;(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;to&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;right&lt;/span&gt;, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;red&lt;/span&gt;, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;green&lt;/span&gt;, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;yellow&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;-webkit-&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;background-clip&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;text&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;-webkit-&lt;/span&gt;text-fill-color: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;transparent&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;background-clip&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;text&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;button&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;font-weight&lt;/span&gt;: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;bold&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;style&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And we can initialize the Vue app and mount the component we just created by adding this to &lt;code&gt;app/frontend/entrypoints/application.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { createApp } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;vue&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; App from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;../components/App.vue&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; app = createApp(App)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;app.mount(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;#app&amp;#39;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And of course, we also have to add the mounting point in &lt;code&gt;app/views/home/index.html.erb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;id&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;app&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The app now looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/06/building-a-web-app-using-rails-8-and-vue-3-with-vite/home-page-with-vue.png&#34; alt=&#34;Home page with Vue&#34;&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it! With Rails, Vue, Vite and a little elbow grease we can put together a beautiful monolithic app, powered by great frameworks that offer a stellar developer experience.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Running Ruby in Podman (When rbenv install Fails on Apple Silicon)</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/01/ruby-in-podman-apple-silicon/"/>
      <id>https://www.endpointdev.com/blog/2026/01/ruby-in-podman-apple-silicon/</id>
      <published>2026-01-12T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/01/ruby-in-podman-apple-silicon/newish-ruins.webp&#34; alt=&#34;Amidst dry, leafless brush, a tall structure of dilapidated bricks stands, with a small archway at the bottom in the center of the image&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025 --&gt;
&lt;p&gt;I recently worked on a legacy Ruby backend app which hadn&amp;rsquo;t been changed for years. A Ruby development environment used to be provided by &lt;a href=&#34;/expertise/version-control-devcamps/&#34;&gt;DevCamps&lt;/a&gt; for this site, but over the years we stopped using Ruby other than this small backend, so we also stopped using camps.&lt;/p&gt;
&lt;p&gt;The frontend development now uses its own local dev server, so I decided to do the same for the backend by running the Unicorn server locally. I&amp;rsquo;m on a MacBook M2, so I should just be able to install &lt;code&gt;rbenv&lt;/code&gt; using Homebrew. Easy, right?&lt;/p&gt;
&lt;p&gt;Unfortunately not. I had a great deal of trouble getting any version of Ruby to run natively on my MacBook M2, so I eventually resorted to using Podman. You can &lt;a href=&#34;#third-try-podman&#34;&gt;skip to the end for my solution&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;first-try-native-rbenv&#34;&gt;First try: native rbenv&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;rbenv&lt;/code&gt; installation went just fine:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;brew install rbenv ruby-build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And after adding &lt;code&gt;eval &amp;quot;$(rbenv init -)&amp;quot;&lt;/code&gt; to my &lt;code&gt;~/.zshrc&lt;/code&gt; file, I tried to install the old version of Ruby:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rbenv install 2.4.10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, this returned a &lt;code&gt;BUILD FAILED&lt;/code&gt; message. Strangely, it also failed with a new version of Ruby (3.3.10). I tried debugging for a while before giving up — I figured this project isn&amp;rsquo;t worth multiple hours of getting a local Ruby installation to work (despite my obstinate attempts to do so).&lt;/p&gt;
&lt;h3 id=&#34;second-try-mise&#34;&gt;Second try: mise&lt;/h3&gt;
&lt;p&gt;Several people on forums &amp;amp; blogs recommended using mise, a language-agnostic version manager. After installing it with Homebrew, I ran:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise use --global ruby@2.4.10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This failed too! There was a good explanation, though: older Ruby versions don&amp;rsquo;t run on ARM processors. Of course, that makes sense. So I just needed to install a newer version from after Apple silicon was supported:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise use --global ruby@3.3.10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, this failed as well, just like rbenv:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise ERROR Failed to install core:ruby@3.3.10:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   0: ~/Library/Caches/mise/ruby/ruby-build/bin/ruby-build exited with non-zero status: exit code 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I&amp;rsquo;m sure there is a way to get Ruby running through a version manager on Apple silicon; I found several &lt;a href=&#34;https://gorails.com/setup/macos/15-sequoia&#34;&gt;tutorials&lt;/a&gt; claiming you can just run these commands as normal and it&amp;rsquo;ll work. However, after spending longer than I&amp;rsquo;d like to admit, I was completely unable to do so despite trying many fixes found online.&lt;/p&gt;
&lt;p&gt;This may be a unique problem on my machine, or I may be missing something. But the fact was, I&amp;rsquo;d waited through about a dozen failed Ruby installs (which take a long time!) and I just needed the app to work now. So I moved to a third option: run Ruby in a container.&lt;/p&gt;
&lt;h3 id=&#34;third-try-podman&#34;&gt;Third try: Podman&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://podman.io/&#34;&gt;Podman&lt;/a&gt; is a fully open-source containerization system which can run Dockerfiles and docker-compose.yml files. The app is a lightweight Sinatra backend with Unicorn as a server, and I just needed to add CloudFlare Turnstile to reduce bot traffic.&lt;/p&gt;
&lt;p&gt;The app uses two main files: &lt;code&gt;unicorn.rb&lt;/code&gt; (which holds the configuration for the Unicorn server) and &lt;code&gt;config.ru&lt;/code&gt; (which defines and runs the Sinatra app). The command to start the app is pretty simple: &lt;code&gt;bundle exec unicorn -c unicorn.rb config.ru&lt;/code&gt;. I also set up two files for the container: &lt;code&gt;Containerfile&lt;/code&gt; and &lt;code&gt;container-compose.yml&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Podman does find files named Dockerfile and docker-compose.yml, but here I&amp;rsquo;m using Podman&amp;rsquo;s convention of Containerfile and container-compose.yml, which takes precedence if both are present in the folder.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;To run this, I just needed to set up the environment in my &lt;code&gt;Containerfile&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&#34;containerfile&#34;&gt;&lt;code&gt;Containerfile&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-containerfile&#34; data-lang=&#34;containerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;FROM&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;ruby:2.4.10&lt;/span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;WORKDIR&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;/usr/src/app&lt;/span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;COPY&lt;/span&gt; . .&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;RUN&lt;/span&gt; mkdir -p /var/log &amp;amp;&amp;amp; &lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;&lt;/span&gt; gem i bundler &amp;amp;&amp;amp; &lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;&lt;/span&gt; bundle install&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;EXPOSE&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;8080&lt;/span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;CMD&lt;/span&gt; [&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;bundle&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;exec&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;unicorn&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;unicorn.rb&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;config.ru&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It&amp;rsquo;s a pretty simple setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set the working directory&lt;/li&gt;
&lt;li&gt;copy files from the source directory into the container&lt;/li&gt;
&lt;li&gt;create the log directory defined in &lt;code&gt;unicorn.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;install bundler&lt;/li&gt;
&lt;li&gt;install gems from Gemfile.lock&lt;/li&gt;
&lt;li&gt;expose the port defined in &lt;code&gt;unicorn.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;define the server start command using a &lt;code&gt;CMD&lt;/code&gt; instruction&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;container-composeyml&#34;&gt;&lt;code&gt;container-compose.yml&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;services&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;backend&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;build&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;context&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;.&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;dockerfile&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;Containerfile&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;volumes&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- .:/usr/src/app&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ports&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;8080:8080&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;volumes&lt;/code&gt; section means that changes on the host machine will be propogated to the container (and vice versa). This means you don&amp;rsquo;t need to rebuild the container when you change the app, instead you can use a change-watching Gem like &lt;a href=&#34;https://github.com/alexch/rerun&#34;&gt;rerun&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: You can run this without a compose file, the compose file just stores the volume &amp;amp; port information so you can run it more easily.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;podman run -d --name mybackend -v .:/usr/src/app -p 8080:8080 &amp;lt;image_id&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;h4 id=&#34;minimal-unicornrb-configuration-file&#34;&gt;Minimal &lt;code&gt;unicorn.rb&lt;/code&gt; configuration file&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;listen &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;0.0.0.0:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;working_directory &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/usr/src/app&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;0.0.0.0&lt;/code&gt; to connect to the container&amp;rsquo;s network interface&lt;/li&gt;
&lt;li&gt;Define our listening port&lt;/li&gt;
&lt;li&gt;Set the working directory to the same value as in our &lt;code&gt;Containerfile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: When we forward port 8080 to our Podman container, that traffic arrives on the container&amp;rsquo;s network interface, not on its internal loopback (localhost). If your app only listens on localhost, it&amp;rsquo;s essentially saying &amp;ldquo;I only accept connections from myself,&amp;rdquo; so the forwarded traffic gets rejected. By binding to 0.0.0.0, your app listens on all interfaces, which includes the one Podman uses to route external traffic into the container.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h4 id=&#34;simple-configru&#34;&gt;Simple &lt;code&gt;config.ru&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;require &amp;#39;sinatra&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;get &amp;#39;/&amp;#39; do
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;Hello from Sinatra in Podman!\n&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;run Sinatra::Application&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is also very simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;After importing sinatra, define a GET route for the &lt;code&gt;/&lt;/code&gt; location (so this will be accessed from &lt;code&gt;localhost:8080/&lt;/code&gt; on the host machine)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ podman build .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ podman run -p 8080:8080 &amp;lt;image-id&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This returned a successful response on my host machine:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl localhost:8080
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Hello from Sinatra in Podman!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When the app failed to run, it kept trying to restart until I moved a .env file into place and it worked. That shows that the volume is working correctly.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not a Ruby expert, but with this setup I was able to convert the JavaScript code for Turnstile to Ruby and get it working!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Quick and Dirty Admin Pages with Rails scaffold_controller</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/12/quick-admin-pages-with-rails-scaffold/"/>
      <id>https://www.endpointdev.com/blog/2025/12/quick-admin-pages-with-rails-scaffold/</id>
      <published>2025-12-29T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/12/quick-admin-pages-with-rails-scaffold/fire-escape-looking-down.webp&#34; alt=&#34;Through the metal grid of a fire escape step, cars and a street far below are visible. On the step are two strong intersecting shadows from the top left, one going rightward and one going downward and to the left&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen --&gt;
&lt;p&gt;You&amp;rsquo;re building a Rails application, and you&amp;rsquo;ve reached that point where you or your team needs a simple way to manage data. You need an admin interface. You&amp;rsquo;ve heard of heavy-weight solutions like ActiveAdmin or Rails Admin, but they feel like overkill for adding a few new categories or moderating user posts. You don&amp;rsquo;t want to spend an afternoon configuring a gem. You need something now.&lt;/p&gt;
&lt;p&gt;Luckily Rails has a built-in lightning-fast way to generate a fully functional admin CRUD interface for any existing model. Meet the often-overlooked scaffold_controller generator. It&amp;rsquo;s the secret weapon for building &amp;ldquo;quick and dirty&amp;rdquo; internal tools.&lt;/p&gt;
&lt;h3 id=&#34;what-is-scaffold_controller-and-why-use-it&#34;&gt;What Is scaffold_controller and Why Use It?&lt;/h3&gt;
&lt;p&gt;When you run &lt;code&gt;rails generate scaffold Post&lt;/code&gt;, it creates everything: the model, migration, controller, and views. The scaffold_controller generator does only half of that, creating only the controller and views (as long as the model already exists).&lt;/p&gt;
&lt;p&gt;This is perfect for creating a separate admin namespace because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s Non-Destructive:&lt;/strong&gt; Your existing, user-facing PostsController remains untouched&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s Fast:&lt;/strong&gt; One command gives you a complete set of CRUD actions and ERB views&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s Simple:&lt;/strong&gt; No new gems, dependencies, or complex configurations to learn&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;lets-build-an-admin-interface-for-a-product-model&#34;&gt;Let&amp;rsquo;s Build an Admin Interface for a Product Model&lt;/h3&gt;
&lt;p&gt;Imagine you have a Product model in your application. You have a ProductsController for your main site, but now you need an admin area to create, edit, and delete products.&lt;/p&gt;
&lt;h4 id=&#34;step-1-create-the-admin-namespace-and-route&#34;&gt;Step 1: Create the Admin Namespace and Route&lt;/h4&gt;
&lt;p&gt;First, we&amp;rsquo;ll set up a routing namespace to keep our admin logic separate. This will put our URLs in this format: &lt;code&gt;/admin/products&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open your &lt;code&gt;config/routes.rb&lt;/code&gt; file and add:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/routes.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.application.routes.draw &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ... existing routes ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  namespace &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:admin&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resources &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:products&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running &lt;code&gt;rails routes&lt;/code&gt; will now show new routes like &lt;code&gt;admin_products_path&lt;/code&gt;, &lt;code&gt;edit_admin_product_path&lt;/code&gt;, etc.&lt;/p&gt;
&lt;h4 id=&#34;step-2-generate-the-scaffold-controller&#34;&gt;Step 2: Generate the Scaffold Controller&lt;/h4&gt;
&lt;p&gt;Now for the magic. In your terminal, run &lt;code&gt;rails generate scaffold_controller Admin::Product&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Important notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The namespace (Admin::) must match the one you used in your routes&lt;/li&gt;
&lt;li&gt;The model name (Product) must be singular, just like with a regular scaffold&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This one command generates several files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Controller:&lt;/strong&gt; &lt;code&gt;app/controllers/admin/products_controller.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Views:&lt;/strong&gt; A full set of ERB templates in &lt;code&gt;app/views/admin/products/&lt;/code&gt; (&lt;code&gt;index.html.erb&lt;/code&gt;, &lt;code&gt;show.html.erb&lt;/code&gt;, &lt;code&gt;new.html.erb&lt;/code&gt;, &lt;code&gt;edit.html.erb&lt;/code&gt;, &lt;code&gt;_form.html.erb&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;step-3-update-the-generated-controller&#34;&gt;Step 3: Update the Generated Controller&lt;/h4&gt;
&lt;p&gt;The generated controller is a great start but it needs one crucial tweak. It doesn&amp;rsquo;t know about our namespace, so it will look for the Product model in the wrong place. The key change is ensuring every reference to the model is to Product, not Admin::Product.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;products_controller.rb&lt;/code&gt; and change the class definition and any model references. Here&amp;rsquo;s a simplified example of what it should look like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/admin/products_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Admin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ProductsController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    before_action &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:set_product&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;only&lt;/span&gt;: [&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:show&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:edit&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:update&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:destroy&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# GET /admin/products&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;index&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#33b&#34;&gt;@products&lt;/span&gt; = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Product&lt;/span&gt;.all
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ... other actions ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;set_product&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#33b&#34;&gt;@product&lt;/span&gt; = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Product&lt;/span&gt;.find(params[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:id&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;product_params&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        params.require(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:product&lt;/span&gt;).permit(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:name&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:description&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:price&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:stock&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;step-4-add-a-link-and-access-control&#34;&gt;Step 4: Add a Link and Access Control&lt;/h4&gt;
&lt;p&gt;Your admin interface is now live at &lt;code&gt;http://localhost:3000/admin/products&lt;/code&gt;. Before you deploy you must add some form of access control. This can be as simple as a basic HTTP authentication check in your Admin::ProductsController.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/admin/products_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Admin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ProductsController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    http_basic_authenticate_with &lt;span style=&#34;color:#038&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ENV&lt;/span&gt;[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ADMIN_USER&amp;#39;&lt;/span&gt;], &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;password&lt;/span&gt;: &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ENV&lt;/span&gt;[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ADMIN_PASSWORD&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ... rest of the controller code ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It would also be good to add a link in your navigation for easy access:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;%= link_to &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Admin Products&amp;#34;&lt;/span&gt;, admin_products_path &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.env.development? %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;when-should-you-use-this-method&#34;&gt;When Should You Use This Method?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Ideal for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internal tools, administrative, and support functions&lt;/li&gt;
&lt;li&gt;Rapid prototyping&lt;/li&gt;
&lt;li&gt;Simple CRUD needs where a full-blown admin gem is overkill&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not ideal for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customer-facing admin panels that need complex filtering, dashboards, or custom forms&lt;/li&gt;
&lt;li&gt;Applications where you need fine-grained, role-based permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The scaffold_controller generator is a fantastic piece of Rails tooling that is often overlooked. It embodies the Rails philosophy of providing quick solutions for common problems. In just a few minutes you can have a functional, separate admin area for any model without the overhead of a new gem or the risk of breaking your public-facing code. So next time you need a simple data management interface, skip the gem research and let scaffold_controller do the heavy lifting for you.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Using ActiveSupport::​ExecutionContext to improve Rails logging</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/12/execution-context-to-improve-logging/"/>
      <id>https://www.endpointdev.com/blog/2025/12/execution-context-to-improve-logging/</id>
      <published>2025-12-05T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/12/execution-context-to-improve-logging/northern-lights-over-utah-valley.webp&#34; alt=&#34;The northern lights glow red above a mountain valley filled with city lights&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;If you’ve ever tried to debug a complex user workflow in a modern Rails application, you know how difficult it can be. A single web request can spawn multiple background jobs, Turbo Stream updates, and a flurry of database queries. Answering the  question &amp;ldquo;What was this user doing?&amp;rdquo; should be simple, but tracing the flow of events through a web of log files can be difficult and time consuming.&lt;/p&gt;
&lt;p&gt;You can add custom log tags but that gets tedious and messy fast. Fortunately, Rails has a powerful, under-documented feature designed specifically to address this problem: ActiveSupport::ExecutionContext.&lt;/p&gt;
&lt;p&gt;In this post, we&amp;rsquo;ll walk through what ExecutionContext is and how you can use it to add meaningful structure to your logs, making debugging a much more straightforward task.&lt;/p&gt;
&lt;h3 id=&#34;the-problem-a-tangled-web-of-logs&#34;&gt;The Problem: A Tangled Web of Logs&lt;/h3&gt;
&lt;p&gt;Imagine a user places an order on your site. The &lt;code&gt;OrdersController#create&lt;/code&gt; action fires, which then enqueues a &lt;code&gt;ReceiptJob&lt;/code&gt; and an &lt;code&gt;InventoryUpdateJob&lt;/code&gt;. The controller also renders a Turbo Stream to update the UI. You have at least four separate units of work: the HTTP request and three background tasks.&lt;/p&gt;
&lt;p&gt;Now, if the &lt;code&gt;InventoryUpdateJob&lt;/code&gt; fails, your log might show an exception, but it won&amp;rsquo;t immediately tell you which user&amp;rsquo;s order triggered it. You&amp;rsquo;re left grepping for job IDs or tracing timestamps. ExecutionContext solves this problem by providing a shared context that is automatically shared across these different units of work.&lt;/p&gt;
&lt;h3 id=&#34;how-executioncontext-works&#34;&gt;How ExecutionContext Works&lt;/h3&gt;
&lt;p&gt;Think of ActiveSupport::ExecutionContext as a container for data that automatically gets passed along. When you store something in it during a web request, that data is bundled up and made available in any background jobs you start from that request without you having to manually send it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important to note that while ActiveSupport::ExecutionContext values are available within the same process (e.g., during the web request), they don&amp;rsquo;t automatically serialize and propagate to background jobs. For the context to be available in jobs, you&amp;rsquo;ll need to explicitly pass relevant values as job arguments. In our example, since we&amp;rsquo;re already passing the order object to both jobs, we can access &lt;code&gt;order.user_id&lt;/code&gt; and &lt;code&gt;order.id&lt;/code&gt; directly in the job. For contexts without such natural carriers consider extracting the relevant values and passing them explicitly as additional job arguments.&lt;/p&gt;
&lt;p&gt;This is the magic that allows you to trace a chain of events.&lt;/p&gt;
&lt;h3 id=&#34;a-practical-example-tracing-an-order&#34;&gt;A Practical Example: Tracing an Order&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s implement a solution for the ordering scenario. Our goal is to tag every log line related to this specific order with the user&amp;rsquo;s ID and the order ID.&lt;/p&gt;
&lt;p&gt;First, we&amp;rsquo;ll set the context in an &lt;code&gt;around_action&lt;/code&gt; in our &lt;code&gt;ApplicationController&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/application_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActionController&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Base&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  around_action &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:set_execution_context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;set_execution_context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# We only set the context if a user is logged in.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; current_user
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:user_id&lt;/span&gt;] = current_user.id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# We can also trace requests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:request_id&lt;/span&gt;] = request.request_id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;yield&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;ensure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Ensure the context is cleared after the request is done.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;.clear
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/orders_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrdersController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;create&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt; = current_user.orders.create!(order_params)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Update the context with the real order_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:order_id&lt;/span&gt;] = &lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt;.id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ReceiptJob&lt;/span&gt;.perform_later(&lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;InventoryUpdateJob&lt;/span&gt;.perform_later(&lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    respond_to &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |&lt;span style=&#34;color:#038&#34;&gt;format&lt;/span&gt;|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#038&#34;&gt;format&lt;/span&gt;.turbo_stream
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Because we used &lt;code&gt;perform_later&lt;/code&gt;, Active Job will automatically serialize our &lt;code&gt;ExecutionContext&lt;/code&gt; (containing &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;order_id&lt;/code&gt;) and make it available when the job runs.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at the &lt;code&gt;InventoryUpdateJob&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/jobs/inventory_update_job.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;InventoryUpdateJob&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationJob&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;perform&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger.info &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Updating inventory for order&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ... your business logic ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;making-the-context-visible-in-logs&#34;&gt;Making the Context Visible in Logs&lt;/h3&gt;
&lt;p&gt;Setting the context is only half the battle; we also need to see it in our logs. We can do this by customizing Rails&amp;rsquo;s log formatter.&lt;/p&gt;
&lt;p&gt;Here’s a simple formatter that appends the execution context to every log line:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/initializers/log_formatting.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ContextAwareFormatter&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Logger&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Formatter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;call&lt;/span&gt;(severity, time, progname, msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;.to_h
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tags = context.map { |key, value| &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;key&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;value&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt; }.join(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tags = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;[&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;tags&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;] &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;unless&lt;/span&gt; tags.empty?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;time.utc.iso8601(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3&lt;/span&gt;)&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;severity&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; #&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Process&lt;/span&gt;.pid&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;tags&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}#{&lt;/span&gt;msg2str(msg)&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Apply it to the Rails logger&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.logger.formatter = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ContextAwareFormatter&lt;/span&gt;.new&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this in place, a log line from our &lt;code&gt;InventoryUpdateJob&lt;/code&gt; might now look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2025-12-01T15:33:01.123 INFO #123 [user_id=1001 order_id=998842] Updating inventory for order&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If that job fails, the stack trace will be prefixed with the same &lt;code&gt;[user_id=1001 order_id=998842]&lt;/code&gt; context. You can instantly see which user and order were affected.&lt;/p&gt;
&lt;h3 id=&#34;going-beyond-tagging-database-queries&#34;&gt;Going Beyond: Tagging Database Queries&lt;/h3&gt;
&lt;p&gt;One of the most powerful applications is tagging database queries. This is incredibly useful for identifying expensive queries related to a specific user in a production environment.&lt;/p&gt;
&lt;p&gt;You can subscribe to the SQL event and include the context in the log:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/initializers/log_formatting.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Notifications&lt;/span&gt;.subscribe(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;sql.active_record&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |event|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  context = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;.to_h
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; context.empty?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  payload = event.payload
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  sql_with_context = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/* &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;context.map &lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt; |k, v| &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;k&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;v&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;.join(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;, &amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; */ &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;payload[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:sql&lt;/span&gt;]&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.logger.debug(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;SQL: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;sql_with_context&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here is what that log line would look like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DEBUG -- : SQL: /* user_id:1001, order_id:998842 */ SELECT &amp;#34;orders&amp;#34;.* FROM &amp;#34;orders&amp;#34; WHERE &amp;#34;orders&amp;#34;.&amp;#34;user_id&amp;#34; = $1 LIMIT $2  [[&amp;#34;user_id&amp;#34;, 1001], [&amp;#34;LIMIT&amp;#34;, 1]]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The SQL notification subscriber will fire on every database query which in production could introduce noticeable overhead. Consider wrapping this functionality in a &lt;code&gt;Rails.env.development?&lt;/code&gt; check or using feature flags to control its activation. Be mindful of this trade-off when adding context to high-volume SQL logging.&lt;/p&gt;
&lt;h3 id=&#34;a-stitch-in-time-saves-nine&#34;&gt;A Stitch in Time Saves Nine&lt;/h3&gt;
&lt;p&gt;ActiveSupport::ExecutionContext is a robust solution to a common problem in modern, event-driven Rails applications. It provides a clean, built-in mechanism for propagating context, which leads to more debuggable and observable systems.&lt;/p&gt;
&lt;p&gt;The next time you find yourself lost in a sea of log files, remember this tool. By adding a few lines of code to set the context and customizing your log formatter, you can transform a chaotic log file into a well-organized story of what your application is doing for each and every user.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Building Offline-Capable Rails Apps Using Service Workers and Turbo</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/08/offline-capable-rails/"/>
      <id>https://www.endpointdev.com/blog/2025/08/offline-capable-rails/</id>
      <published>2025-08-07T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/08/offline-capable-rails/red-flowers.webp&#34; alt=&#34;The left half of the image is dominated by out-of-focus pink-red flower petals close to the camera, which give way to petals on branches which are in focus, with sunlight shining through the back.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen on Karmir 160 film, 2025. --&gt;
&lt;p&gt;Users today expect web apps to continue working even when their internet connection is unstable or temporarily lost. Out of the box, Rails applications do not handle this well. In this blog post, we will walk through how to add offline support to a Rails app using Service Workers, Workbox, and Turbo. This approach gives users a smoother experience and better reliability when network conditions are not ideal.&lt;/p&gt;
&lt;p&gt;This guide focuses on a real-world setup. You will learn how to cache pages and assets, handle Turbo form submissions offline, and provide a fallback UI when needed.&lt;/p&gt;
&lt;h3 id=&#34;why-offline-support-matters&#34;&gt;Why Offline Support Matters&lt;/h3&gt;
&lt;p&gt;Turbo makes Rails applications fast and responsive by replacing traditional client-side JavaScript with HTML over the wire. However, if the network drops out, those Turbo requests fail silently. Adding a Service Worker allows us to intercept those requests and provide a better experience.&lt;/p&gt;
&lt;p&gt;Offline support improves performance, enhances user experience on mobile devices, and adds resilience in situations where network access is intermittent.&lt;/p&gt;
&lt;h3 id=&#34;creating-a-manifest-file&#34;&gt;Creating a Manifest File&lt;/h3&gt;
&lt;p&gt;Start by adding a manifest file to your Rails app. This file lets the browser know your app supports offline behavior.&lt;/p&gt;
&lt;p&gt;Create a file at &lt;code&gt;app/assets/manifest.json&lt;/code&gt; with the following content:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Offline Rails App&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;short_name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;OfflineRails&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;start_url&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;display&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;standalone&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;background_color&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;#ffffff&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;theme_color&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;#222222&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, in your application layout, reference the manifest file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;%= tag.link rel: &amp;#34;manifest&amp;#34;, href: asset_path(&amp;#34;manifest.json&amp;#34;) %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This enables progressive web app features and prepares your app for Service Worker support.&lt;/p&gt;
&lt;h3 id=&#34;creating-the-service-worker&#34;&gt;Creating the Service Worker&lt;/h3&gt;
&lt;p&gt;Rails does not ship with a Service Worker, so you need to create one. You can either output the compiled Service Worker file directly into the &lt;code&gt;public&lt;/code&gt; folder or configure your JavaScript build system to handle it.&lt;/p&gt;
&lt;p&gt;Here is a minimal Service Worker example using Workbox, saved as &lt;code&gt;public/service-worker.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { precacheAndRoute } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;workbox-precaching&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { registerRoute } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;workbox-routing&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { NetworkFirst, CacheFirst } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;workbox-strategies&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;precacheAndRoute(self.__WB_MANIFEST || [])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;registerRoute(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ({ request }) =&amp;gt; request.mode === &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;navigate&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; NetworkFirst()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;registerRoute(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  ({ request }) =&amp;gt; [&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;style&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;script&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;image&amp;#39;&lt;/span&gt;].includes(request.destination),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CacheFirst()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This configuration tells the browser to try the network first for page navigation, but to fall back to cache if offline. Static assets such as stylesheets, scripts, and images are cached after the first visit.&lt;/p&gt;
&lt;h3 id=&#34;registering-the-service-worker-in-your-rails-app&#34;&gt;Registering the Service Worker in Your Rails App&lt;/h3&gt;
&lt;p&gt;To register the Service Worker, add this code to your Rails JavaScript entry point. In Rails 7 with import maps or jsbundling, this is usually found in &lt;code&gt;application.js&lt;/code&gt; or &lt;code&gt;app/javascript/application.js&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;serviceWorker&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;in&lt;/span&gt; navigator) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#038&#34;&gt;window&lt;/span&gt;.addEventListener(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;load&amp;#39;&lt;/span&gt;, () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    navigator.serviceWorker.register(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/service-worker.js&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .then(reg =&amp;gt; console.log(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Service worker registered&amp;#39;&lt;/span&gt;, reg))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      .&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt;(err =&amp;gt; console.error(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Registration failed&amp;#39;&lt;/span&gt;, err))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once registered, the browser begins managing network requests through the Service Worker.&lt;/p&gt;
&lt;h3 id=&#34;pre-caching-essential-pages&#34;&gt;Pre-Caching Essential Pages&lt;/h3&gt;
&lt;p&gt;To ensure key pages are available offline before a user visits them, you can pre-cache them during the install phase of the Service Worker.&lt;/p&gt;
&lt;p&gt;Add the following to your Service Worker file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;self.addEventListener(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;install&amp;#39;&lt;/span&gt;, event =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  event.waitUntil(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    caches.open(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;static-pages-v1&amp;#39;&lt;/span&gt;).then(cache =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; cache.addAll([
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/important_things&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/offline.html&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      ])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These pre-cached pages are useful for first-time offline users and can be extended as needed.&lt;/p&gt;
&lt;h3 id=&#34;handling-offline-turbo-form-submissions&#34;&gt;Handling Offline Turbo Form Submissions&lt;/h3&gt;
&lt;p&gt;Turbo uses fetch to submit forms. If the network is down, the request will fail and the user may not even notice. To improve this, you can intercept Turbo form submissions using Stimulus and queue them locally.&lt;/p&gt;
&lt;p&gt;Here is an example Stimulus controller:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { Controller } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;@hotwired/stimulus&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;extends&lt;/span&gt; Controller {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  connect() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;this&lt;/span&gt;.element.addEventListener(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;turbo:submit-start&amp;#34;&lt;/span&gt;, event =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (!navigator.onLine) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        event.preventDefault()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; formData = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; FormData(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;this&lt;/span&gt;.element)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; payload = &lt;span style=&#34;color:#038&#34;&gt;Object&lt;/span&gt;.fromEntries(formData)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        queueOfflineSubmission(payload)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        alert(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;You are offline. Your submission has been saved.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You will need to define &lt;code&gt;queueOfflineSubmission&lt;/code&gt; to save the data to IndexedDB or localStorage. Later, you can sync the data when the connection is restored.&lt;/p&gt;
&lt;h3 id=&#34;adding-an-offline-fallback-page&#34;&gt;Adding an Offline Fallback Page&lt;/h3&gt;
&lt;p&gt;If a request fails because the user is offline and the response is not cached, it is helpful to show a fallback page.&lt;/p&gt;
&lt;p&gt;Add this to your Service Worker:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;self.addEventListener(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;fetch&amp;#39;&lt;/span&gt;, event =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  event.respondWith(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    fetch(event.request).&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt;(() =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; caches.match(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/offline.html&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;})&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Place a simple &lt;code&gt;offline.html&lt;/code&gt; file in the &lt;code&gt;public&lt;/code&gt; folder. This helps users understand what happened instead of seeing a generic browser error.&lt;/p&gt;
&lt;h3 id=&#34;summary&#34;&gt;Summary&lt;/h3&gt;
&lt;p&gt;Adding offline support to a Rails application is very achievable using Turbo, Service Workers, and a small amount of JavaScript. With this setup, your app can handle dropped connections without frustrating your users.&lt;/p&gt;
&lt;p&gt;You now have a working foundation that includes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A browser manifest and Service Worker registration&lt;/li&gt;
&lt;li&gt;Cached pages and assets for offline use&lt;/li&gt;
&lt;li&gt;Form submission queuing while offline&lt;/li&gt;
&lt;li&gt;A fallback UI for missing network responses&lt;/li&gt;
&lt;/ul&gt;

      </content>
    </entry>
  
    <entry>
      <title>Integrating Grape with Sidekiq for Asynchronous Processing in Rails</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/08/integrating-grape-with-sidekiq-for-asynchronous-processing-in-rails/"/>
      <id>https://www.endpointdev.com/blog/2025/08/integrating-grape-with-sidekiq-for-asynchronous-processing-in-rails/</id>
      <published>2025-08-04T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/08/integrating-grape-with-sidekiq-for-asynchronous-processing-in-rails/mountain-path.webp&#34; alt=&#34;A mountain path leading into an aspen forest, with light shining from behind the trees. The image has a red cast, especially on the haze glowing around the leaves from the afternoon sun.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen on Karmir 160, 2025. --&gt;
&lt;p&gt;When building an API in a Rails application using Grape, there is a natural tendency to handle all logic synchronously. For small applications or internal tools, that can work well. But for a growing SaaS platform or public-facing API, doing too much work in the request cycle leads to slower response times and can make your endpoints fragile.&lt;/p&gt;
&lt;p&gt;This post walks through the process of integrating Grape with Sidekiq so that heavy tasks can be moved to background workers. The goal is to keep endpoints fast and resilient while offloading expensive processing.&lt;/p&gt;
&lt;h3 id=&#34;why-asynchronous-processing-matters&#34;&gt;Why asynchronous processing matters&lt;/h3&gt;
&lt;p&gt;Many common tasks in an API do not need to be done immediately. Examples include sending confirmation emails, syncing to a third-party service, exporting data, or generating reports. By sending these jobs to a background processor like Sidekiq, the API can respond quickly and let the user continue without waiting.&lt;/p&gt;
&lt;p&gt;Separating these tasks also gives better observability and error handling. Sidekiq offers retries, job tracking, and simple ways to inspect queues out of the box.&lt;/p&gt;
&lt;h3 id=&#34;setting-up-grape-and-sidekiq&#34;&gt;Setting up Grape and Sidekiq&lt;/h3&gt;
&lt;p&gt;Assuming you already have a Rails application with Grape installed, the next step is to add Sidekiq and Redis. Redis is used to store the job queue.&lt;/p&gt;
&lt;p&gt;Add the following to your Gemfile:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;grape&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;sidekiq&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt; and start Redis if it is not already running.&lt;/p&gt;
&lt;p&gt;Next, configure Rails to use Sidekiq for background jobs by setting the queue adapter in &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config.active_job.queue_adapter = &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:sidekiq&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also add a basic Sidekiq YAML config file to control queue priorities:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/sidekiq.yml&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;:queues&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;- default&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;creating-a-sidekiq-worker&#34;&gt;Creating a Sidekiq worker&lt;/h3&gt;
&lt;p&gt;To use Sidekiq directly, create a new file in &lt;code&gt;app/workers&lt;/code&gt;. For example, to send a welcome email:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;WelcomeEmailWorker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Sidekiq&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Worker&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;perform&lt;/span&gt;(this_username)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    user = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;User&lt;/span&gt;.find_by(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;username&lt;/span&gt;: this_username)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;UserMailer&lt;/span&gt;.welcome(user).deliver_now
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This worker fetches a user and sends an email using a standard Rails mailer. The goal here is to keep logic focused and avoid branching inside the job.&lt;/p&gt;
&lt;h3 id=&#34;triggering-a-background-job-from-a-grape-endpoint&#34;&gt;Triggering a background job from a Grape endpoint&lt;/h3&gt;
&lt;p&gt;Now we connect the worker to a Grape endpoint. Here is an example of an endpoint that creates a user and queues a welcome email to be sent later:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/api/v1/users_api.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;V1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;UsersAPI&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Grape&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    version &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;v1&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;using&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:path&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;format&lt;/span&gt; &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resource &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:users&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      desc &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Create a user and queue welcome email&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      params &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        requires &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#038&#34;&gt;String&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;desc&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Users email&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        requires &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:name&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#038&#34;&gt;String&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;desc&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Users name&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        requires &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:username&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;type&lt;/span&gt;: &lt;span style=&#34;color:#038&#34;&gt;String&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;desc&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Users username&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      post &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        user = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;User&lt;/span&gt;.create!(declared(params))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Enqueue welcome email&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;WelcomeEmailJob&lt;/span&gt;.perform_later(user.username)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        status &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;201&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        { &lt;span style=&#34;color:#038&#34;&gt;id&lt;/span&gt;: user.id, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;email&lt;/span&gt;: user.email, &lt;span style=&#34;color:#038&#34;&gt;name&lt;/span&gt;: user.name, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;username&lt;/span&gt;: user.username }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once the user is saved to the database, the &lt;code&gt;perform_async&lt;/code&gt; method queues the job to be picked up by a running Sidekiq worker process.&lt;/p&gt;
&lt;p&gt;This lets the API return a response right away instead of waiting for the email to be sent. In a production environment, this difference becomes more noticeable when there are many different parts to the request, or many requests to be triggered.&lt;/p&gt;
&lt;h3 id=&#34;running-sidekiq&#34;&gt;Running Sidekiq&lt;/h3&gt;
&lt;p&gt;To process jobs, Sidekiq must be running in a separate process from your Rails app server. In development, you can run it with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bundle exec sidekiq&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You should see logs indicating that the worker has started and is listening for jobs on the default queue.&lt;/p&gt;
&lt;h3 id=&#34;error-handling-and-retries&#34;&gt;Error handling and retries&lt;/h3&gt;
&lt;p&gt;Sidekiq includes built-in retry logic. You can configure the number of retries or disable them for specific jobs.&lt;/p&gt;
&lt;p&gt;To log errors explicitly or report them to an external service, wrap your job logic in a begin-rescue block:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;perform&lt;/span&gt;(this_username)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  user = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;User&lt;/span&gt;.find_by(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;username&lt;/span&gt;: this_username)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;UserMailer&lt;/span&gt;.welcome(user).deliver_now
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;rescue&lt;/span&gt; =&amp;gt; e
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.logger.error(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Welcome email failed: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;e.message&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;raise&lt;/span&gt; e
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Raising the error ensures that Sidekiq still sees the job as failed and can retry it if needed.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Integrating Grape with Sidekiq is a simple but effective way to improve performance and reliability in a Rails API. It lets you keep endpoint logic clean and offload work that does not need to be completed immediately.&lt;/p&gt;
&lt;p&gt;This approach scales well for APIs that process uploads, send notifications, or interact with third-party services. It also aligns with the principle of keeping the request-response cycle short and predictable.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re working with Grape in a growing Rails project, this is a natural next step.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Rebuilding a Modern App in Rails 7 Without JavaScript Frameworks</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/05/rebuilding-a-modern-app-in-rails-7-without-js/"/>
      <id>https://www.endpointdev.com/blog/2025/05/rebuilding-a-modern-app-in-rails-7-without-js/</id>
      <published>2025-05-13T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/05/rebuilding-a-modern-app-in-rails-7-without-js/liverpool-waterfront.webp&#34; alt=&#34;A singler seagull flies in front of a subtle sunset, over buildings and a smokestack forming a skyline above water.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2024. --&gt;
&lt;p&gt;In the realm of web development, the allure of JavaScript frameworks like React and Vue is undeniable. However, I recently embarked on a mission to rebuild a modern web application using Rails 7, Hotwire, and Turbo, deliberately avoiding any JavaScript frameworks. The outcome was a streamlined stack, improved performance, and a more maintainable codebase.&lt;/p&gt;
&lt;h3 id=&#34;why-consider-a-framework-free-approach-in-2025&#34;&gt;Why Consider a Framework-Free Approach in 2025?&lt;/h3&gt;
&lt;p&gt;The complexity introduced by modern JavaScript frameworks can sometimes overshadow their benefits. Managing dependencies, build tools, and the intricacies of client-side rendering often lead to increased development overhead. With the advancements in Rails 7, particularly the introduction of &lt;a href=&#34;https://hotwired.dev/&#34;&gt;Hotwire&lt;/a&gt; and &lt;a href=&#34;https://turbo.hotwired.dev/&#34;&gt;Turbo&lt;/a&gt;, it&amp;rsquo;s now feasible to build dynamic, responsive applications without the need for additional JavaScript frameworks.&lt;/p&gt;
&lt;h3 id=&#34;the-application-a-simplified-project-management-tool&#34;&gt;The Application: A Simplified Project Management Tool&lt;/h3&gt;
&lt;p&gt;I had previously built an app that was esentially a simplified version of Trello. The app is a lightweight project management tool featuring:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Boards and cards&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Drag-and-drop functionality&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Real-time updates&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Commenting system&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;User authentication and role management&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Originally built with a React frontend and a Rails API backend, I decided to reconstruct it as a Rails 7 application, using Hotwire and Turbo for interactivity.&lt;/p&gt;
&lt;h3 id=&#34;advantages-of-using-rails-7-with-hotwire-and-turbo&#34;&gt;Advantages of Using Rails 7 with Hotwire and Turbo&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Simplified stack&lt;/strong&gt;: Eliminating the need for separate frontend frameworks reduced complexity and streamlined the development process.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Enhanced performance&lt;/strong&gt;: Server-rendered HTML and Turbo&amp;rsquo;s partial page updates led to faster load times and a more responsive user experience.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Improved maintainability&lt;/strong&gt;: A unified codebase made it easier to manage and scale the application over time.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;challenges-encountered&#34;&gt;Challenges Encountered&lt;/h3&gt;
&lt;p&gt;While the experience was largely positive, there were some hurdles:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Implementing drag-and-drop&lt;/strong&gt;: Achieving this functionality required integrating a lightweight JavaScript library and connecting it with Stimulus controllers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Real-time collaboration&lt;/strong&gt;: Utilizing Turbo Streams and ActionCable facilitated real-time updates, but required careful handling to ensure consistency across clients.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;File uploads&lt;/strong&gt;: Managing file uploads and displaying progress indicators necessitated additional configuration and integration with Active Storage.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;successes-achieved&#34;&gt;Successes Achieved&lt;/h3&gt;
&lt;p&gt;Despite the challenges, several aspects of the rebuild were notably successful:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Modal dialogs with turbo frames&lt;/strong&gt;: Loading forms and content into modals became straightforward, enhancing user interaction without additional JavaScript.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Live updates with turbo streams&lt;/strong&gt;: Real-time broadcasting of updates improved collaboration features and kept users informed without manual refreshes.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Interactive elements with stimulus&lt;/strong&gt;: Implementing dynamic behaviors like toggles and dropdowns was efficient and required minimal JavaScript.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;comparison-to-the-previous-react-based-build&#34;&gt;Comparison to the Previous React-Based Build&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Feature&lt;/th&gt;
          &lt;th&gt;React Version&lt;/th&gt;
          &lt;th&gt;Hotwire Version&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Build complexity&lt;/td&gt;
          &lt;td&gt;High (Webpack, Babel, etc.)&lt;/td&gt;
          &lt;td&gt;Low (No bundler required)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Developer onboarding&lt;/td&gt;
          &lt;td&gt;Steep learning curve&lt;/td&gt;
          &lt;td&gt;More accessible for full-stack devs&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Performance&lt;/td&gt;
          &lt;td&gt;Fast post-hydration&lt;/td&gt;
          &lt;td&gt;Fast initial load with partial updates&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Real-time capabilities&lt;/td&gt;
          &lt;td&gt;Requires state management libraries&lt;/td&gt;
          &lt;td&gt;Built-in with Turbo Streams&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Maintenance overhead&lt;/td&gt;
          &lt;td&gt;Higher due to separate codebases&lt;/td&gt;
          &lt;td&gt;Lower with a unified codebase&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;final-thoughts&#34;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Rebuilding the application using Rails 7 with Hotwire and Turbo proved to be a rewarding experience. The simplified stack, improved performance, and enhanced maintainability made it a compelling approach for developing modern web applications. While certain complex functionalities required additional effort, the overall benefits outweighed the challenges.&lt;/p&gt;
&lt;p&gt;For developers seeking to build responsive and dynamic applications without the overhead of JavaScript frameworks, exploring the capabilities of Rails 7 with Hotwire and Turbo is a great option.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Migrating Rails 6 React to Rails 7 React</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/06/migrating-rails6-react-rails7-react/"/>
      <id>https://www.endpointdev.com/blog/2023/06/migrating-rails6-react-rails7-react/</id>
      <published>2023-06-26T00:00:00+00:00</published>
      <author>
        <name>Indra Pranesh Palanisamy</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/06/migrating-rails6-react-rails7-react/pexels-indra-pranesh-palanisamy-17019134.webp&#34; alt=&#34;A coconut tree stands in the corner of a serene blue sky&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Indra Pranesh Palanisamy, 2022 --&gt;
&lt;p&gt;CasePointer’s disease reporting portal is built on React and Rails 6, and it&amp;rsquo;s time for an upgrade to Rails 7. This blog post will cover the steps, benefits, and challenges of migrating from Rails 6 to Rails 7, and offer valuable insights into the world of Ruby on Rails.&lt;/p&gt;
&lt;p&gt;With the recent release of Rails 7, there are many new features and improvements to explore.
One of the biggest changes in Rails 7 is the retirement of Webpacker in favor of using the native webpack for bundling JavaScript.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For those who are not familiar, Webpacker is a Rails gem which is a wrapper around the webpack build system that provides a standard webpack configuration and reasonable defaults.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;steps-for-migrating-rails-6-react-to-rails-7-react&#34;&gt;Steps for migrating Rails 6 React to Rails 7 React&lt;/h3&gt;
&lt;p&gt;To migrate a Rails 6 React application to Rails 7 React, follow these steps:&lt;/p&gt;
&lt;h4 id=&#34;1-update-the-rails-gem-in-the-gemfile&#34;&gt;1. Update the Rails Gem in the Gemfile&lt;/h4&gt;
&lt;p&gt;In your application&amp;rsquo;s Gemfile, update the Rails gem version to Rails 7:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#fdd&#34;&gt;-gem &amp;#34;rails&amp;#34;, &amp;#34;~&amp;gt; 6.1.4&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#fdd&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+gem &amp;#34;rails&amp;#34;, &amp;#34;~&amp;gt; 7.0.0&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;2-upgrade-rails-packages&#34;&gt;2. Upgrade Rails packages&lt;/h4&gt;
&lt;p&gt;Upgrade the Rails packages using Yarn:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn upgrade @rails/actioncable --latest
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn upgrade @rails/activestorage --latest&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;3-run-the-rails-update-task&#34;&gt;3. Run the Rails update task&lt;/h4&gt;
&lt;p&gt;Run the following command to initiate the Rails update task:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bin/rails app:update&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This task guides you file by file to integrate the new Rails 7 defaults. Refer to the &lt;a href=&#34;https://guides.rubyonrails.org/upgrading_ruby_on_rails.html&#34;&gt;Rails documentation&lt;/a&gt; for detailed information on this update task.&lt;/p&gt;
&lt;h4 id=&#34;4-remove-webpacker&#34;&gt;4. Remove Webpacker&lt;/h4&gt;
&lt;p&gt;Since Webpacker is no longer the default in Rails 7, follow these steps to remove it:&lt;/p&gt;
&lt;p&gt;Remove the webpacker gem from your Gemfile:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#fdd&#34;&gt;-gem &amp;#39;webpacker&amp;#39;, &amp;#39;~&amp;gt; 4.0&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Remove the &lt;code&gt;webpacker.yml&lt;/code&gt; file and any other files associated with webpacker.&lt;/p&gt;
&lt;p&gt;Run &lt;code&gt;bundle install&lt;/code&gt; to update the Gemfile.lock:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bundle install&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;5-setting-up-webpack&#34;&gt;5. Setting up Webpack&lt;/h4&gt;
&lt;p&gt;To set up Webpack for your Rails 7 React application, follow these steps:&lt;/p&gt;
&lt;p&gt;Install webpack and other necessary libraries:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn install webpack webpack-cli @babel/core&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create a webpack.config.js file at the root of your application:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// webpack.config.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; path = require(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;path&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; webpack = require(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;webpack&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Add other necessary plugins and configurations
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;module.exports = {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// Webpack configuration options
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;  output: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Make sure to use the path of the rails asset pipeline
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    path: path.join(__dirname, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/app/assets/builds&amp;#39;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  module: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    rules: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;// Add CSS/SASS/SCSS rule with loaders
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;      {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test: &lt;span style=&#34;color:#080;background-color:#fff0ff&#34;&gt;/\.(?:sa|sc|c)ss$/i&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        use: [MiniCssExtractPlugin.loader, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;css-loader&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;sass-loader&amp;#39;&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test: &lt;span style=&#34;color:#080;background-color:#fff0ff&#34;&gt;/\.(png|jpe?g|gif|eot|woff2|woff|ttf|svg)$/i&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        use: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;file-loader&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        test: &lt;span style=&#34;color:#080;background-color:#fff0ff&#34;&gt;/\.(js|jsx)$/&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        exclude: &lt;span style=&#34;color:#080;background-color:#fff0ff&#34;&gt;/node_modules/&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        use: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          loader: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;babel-loader&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Add a .babelrc file for Babel configuration:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;presets&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;@babel/preset-env&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;@babel/preset-react&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Update the scripts section in your package.json:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;build&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;webpack&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;dev&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;webpack --watch&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;6-update-rails-asset-tags&#34;&gt;6. Update Rails asset tags&lt;/h4&gt;
&lt;p&gt;In your application views, update the asset tags from &lt;code&gt;stylesheet_pack_tag&lt;/code&gt; and &lt;code&gt;javascript_pack_tag&lt;/code&gt; to &lt;code&gt;stylesheet_link_tag&lt;/code&gt; and &lt;code&gt;javascript_include_tag&lt;/code&gt;, respectively:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;%= stylesheet_link_tag &amp;#39;application&amp;#39;, media: &amp;#39;all&amp;#39;, &amp;#39;data-turbolinks-track&amp;#39;: &amp;#39;reload&amp;#39; %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;%= javascript_include_tag &amp;#39;application&amp;#39;, &amp;#39;data-turbolinks-track&amp;#39;: &amp;#39;reload&amp;#39; %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;7-start-the-dev-server&#34;&gt;7. Start the dev server&lt;/h4&gt;
&lt;p&gt;Start the Rails development server with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rails server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then start the React development server with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yarn run dev&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By following these steps, we can take advantage of the new features and improvements in Rails 7 while continuing to leverage the power of React.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>I wrote the same app twice, with Hotwire and Blazor Server — here’s what I learned</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/05/i-wrote-the-same-app-twice-with-hotwire-and-blazor-server/"/>
      <id>https://www.endpointdev.com/blog/2023/05/i-wrote-the-same-app-twice-with-hotwire-and-blazor-server/</id>
      <published>2023-05-27T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/05/i-wrote-the-same-app-twice-with-hotwire-and-blazor-server/2022-09-14_193717.webp&#34; alt=&#34;A dark sky sprawls over a tall canyon. Misty clouds hang on jagged peaks on the hill to the left. The other hill on the right rises at a steep angle, making a &amp;ldquo;V&amp;rdquo; shape. Both hills are covered in different shades of green, mostly a deep, dark green due to the late hour. Some sun peeks through the overcast sky to let through a bit of bluer light.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2022. --&gt;
&lt;p&gt;There&amp;rsquo;s been a very interesting movement that has emerged recently in the world of frontend web development: a rise of little-to-no-JavaScript frontend frameworks.&lt;/p&gt;
&lt;p&gt;The promise here is that we would be able to develop web applications with rich interactive capabilities without the need to write a whole lot of JavaScript. As such, these new approaches present themselves as alternatives to the likes of Vue, React, Angular, etc.&lt;/p&gt;
&lt;p&gt;Two recent technologies that try to fulfill this promise come from two of the most prolific web application development frameworks of today: &lt;a href=&#34;https://dotnet.microsoft.com/en-us/apps/aspnet/web-apps/blazor&#34;&gt;Blazor&lt;/a&gt;, built on .NET, and &lt;a href=&#34;https://hotwired.dev/&#34;&gt;Hotwire&lt;/a&gt;, built on Ruby on Rails.&lt;/p&gt;
&lt;p&gt;Now, I love my JS frameworks as much as the next guy, but these new technologies are intriguing. So I decided to build the same application twice, with Hotwire and with Blazor. I learned a few things along the way that I would like to share in this blog post.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note that there is a &lt;a href=&#34;#table-of-contents&#34;&gt;table of contents&lt;/a&gt; at the end of this post.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;what-this-article-is&#34;&gt;What this article is&lt;/h3&gt;
&lt;p&gt;I want to present some of my findings when working with these two technologies. I also want to discuss how they work and how they feel. How they are similar and how they are different. How they take different routes to arrive at their ultimately similar destinations. Maybe offer some pros and cons.&lt;/p&gt;
&lt;p&gt;This post assumes sufficient familiarity with &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/csharp/&#34;&gt;C#&lt;/a&gt;, &lt;a href=&#34;https://dotnet.microsoft.com/en-us/apps/aspnet&#34;&gt;ASP.NET&lt;/a&gt;, &lt;a href=&#34;https://www.ruby-lang.org/en/&#34;&gt;Ruby&lt;/a&gt;, &lt;a href=&#34;https://rubyonrails.org/&#34;&gt;Ruby on Rails&lt;/a&gt; and the current state of the art of web development. I won&amp;rsquo;t assume any familiarity with either Blazor or Hotwire, but this is not a tutorial for either, so I won&amp;rsquo;t explain in detail how to fully build apps with these technologies.&lt;/p&gt;
&lt;p&gt;So who am I writing this for? Essentially, for anybody who is curious about these technologies and is interested in understanding the big picture of what they are about, how they compare to each other, and building their next project with one of them. So, this article is intended to serve more as an introduction to both, a starting point for a conversation to help you make a decision on what&amp;rsquo;s best for you and your team.&lt;/p&gt;
&lt;p&gt;Spoiler alert: Both are great and you can&amp;rsquo;t go wrong with either. It all comes down to your team&amp;rsquo;s preferences and past experience.&lt;/p&gt;
&lt;p&gt;One final thing worth noting is that I&amp;rsquo;m focusing this article on &amp;ldquo;&lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-7.0#blazor-server&#34;&gt;Blazor Server&lt;/a&gt;&amp;rdquo; specifically. I&amp;rsquo;ll be using the word &amp;ldquo;Blazor&amp;rdquo; moving forward, for short. Blazor as a framework has three variants: Blazor Server, Blazor WebAssembly and Blazor Hybrid. You can learn more about them &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-7.0&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Most of the examples that I&amp;rsquo;ll use in this post come from a couple of demo apps that I built in order to get my feet wet with these technologies. You can find both of them in GitHub. &lt;a href=&#34;https://github.com/megakevin/quote-editor-blazor&#34;&gt;Here&amp;rsquo;s the Blazor one&lt;/a&gt; and &lt;a href=&#34;https://github.com/megakevin/quote-editor-hotwire&#34;&gt;here&amp;rsquo;s the Hotwire one&lt;/a&gt;. You can study both the source code and the commit history, which I tried my best to keep neatly organized. They are both functionally identical, and based on &lt;a href=&#34;https://www.hotrails.dev/turbo-rails&#34;&gt;this excellent Hotwire tutorial&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;an-overview-of-blazor&#34;&gt;An overview of Blazor&lt;/h3&gt;
&lt;p&gt;When it comes to how they are designed and the developer experience they offer, these two are very different. Let&amp;rsquo;s go over some of the key details of Blazor and then we&amp;rsquo;ll do the same with Hotwire. With that, the differences between them will become apparent.&lt;/p&gt;
&lt;p&gt;The first thing we have to understand about Blazor is that it is a component framework, very much like Vue or React. So, with Blazor, applications are broken up into composable modules called &amp;ldquo;&lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/?view=aspnetcore-7.0&#34;&gt;Razor/Blazor components&lt;/a&gt;&amp;rdquo; that are essentially independent pieces of GUI bundled with their corresponding logic. Each component has three parts to it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;HTML-like markup that describes the layout and UI elements to be rendered,&lt;/li&gt;
&lt;li&gt;C# logic that defines the behavior of the component, like what actions to take when users interact with GUI elements, and&lt;/li&gt;
&lt;li&gt;CSS for styling the GUI elements.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example here&amp;rsquo;s a simple a Blazor component that allows displaying, editing, and deleting a particular type of record called &amp;ldquo;Quote&amp;rdquo;. Don&amp;rsquo;t worry about the details too much; we&amp;rsquo;ll go over some of them next. For now, I just want us to get a sense of what Blazor components look like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@using Microsoft.EntityFrameworkCore
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@inject IDbContextFactory&amp;lt;QuoteEditorBlazor.Data.QuoteEditorContext&amp;gt; dbContextFactory
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@if (isEditing)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;QuoteEditorBlazor.Shared.Quotes.Edit
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        QuoteToEdit=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;QuoteToShow&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        OnCancel=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;HideEditForm&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;div class=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;quote&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;a href=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;quotes/@QuoteToShow.ID&amp;#34;&lt;/span&gt;&amp;gt;@QuoteToShow.Name&amp;lt;/a&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;div class=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;quote__actions&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &amp;lt;a class=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;btn btn--light&amp;#34;&lt;/span&gt; @onclick=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;DeleteQuote&amp;#34;&lt;/span&gt;&amp;gt;Delete&amp;lt;/a&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &amp;lt;a class=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;btn btn--light&amp;#34;&lt;/span&gt; @onclick=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;ShowEditForm&amp;#34;&lt;/span&gt;&amp;gt;Edit&amp;lt;/a&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@code {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;    [Parameter]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; QuoteEditorBlazor.Models.Quote QuoteToShow { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;    [Parameter]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; EventCallback OnQuoteDeleted { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;bool&lt;/span&gt; isEditing = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; ShowEditForm()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        isEditing = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; HideEditForm()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        isEditing = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; DeleteQuote()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;using&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;var&lt;/span&gt; context = dbContextFactory.CreateDbContext();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context.Quotes.Remove(QuoteToShow);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        context.SaveChanges();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        OnQuoteDeleted.InvokeAsync();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can see that we have some C# in the file (enclosed in a &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-7.0#code&#34;&gt;&lt;code&gt;@code&lt;/code&gt;&lt;/a&gt; block) with some event handlers and parameters. We also have some markup written with a mixture of HTML and C#. This markup has conditionals, wires up click event handlers, renders data from a given record, renders another component, etc. That markup is really just &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-7.0&#34;&gt;Razor&lt;/a&gt;, a templating language that has been widely used in ASP.NET for a good while now. And we also have some top-level statements like &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-7.0#using&#34;&gt;&lt;code&gt;@using&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-7.0#inject&#34;&gt;&lt;code&gt;@inject&lt;/code&gt;&lt;/a&gt; for including classes and objects that the component can use.&lt;/p&gt;
&lt;p&gt;As far as CSS goes, &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/css-isolation?view=aspnetcore-7.0&#34;&gt;here&amp;rsquo;s how it works&lt;/a&gt;: Suppose these are the contents of a file called &lt;code&gt;Example.razor&lt;/code&gt;. The CSS for it would have to be defined in an &lt;code&gt;Example.razor.css&lt;/code&gt; file sitting right next to it. Syntax-wise, this would just be a plain old CSS file. One cool thing to mention about it, though, is that the CSS within it is visible only to the component. So there&amp;rsquo;s no risk of conflicting rules between components.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re familiar with modern frontend web development and have used frameworks like Vue or React, this should look very familiar to you. In fact, I would venture to say this is one of Blazor&amp;rsquo;s most attractive points. If you come from that background, and know .NET, it&amp;rsquo;s not that big of a leap to get into Blazor. The development experience is very similar as its design shares many concepts with modern JS frameworks; they operate under a very similar mental model.&lt;/p&gt;
&lt;p&gt;Of course, C# is not JavaScript and .NET is not a browser. So there are many differences when it comes to the nitty-gritty mechanics of things. So a thorough understanding of the .NET framework and its &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/standard/framework-libraries&#34;&gt;libraries&lt;/a&gt; is also important for mastering Blazor. Which, depending on your team&amp;rsquo;s background, may be a point in favor or against.&lt;/p&gt;
&lt;h4 id=&#34;how-blazor-works-under-the-hood&#34;&gt;How Blazor works under the hood&lt;/h4&gt;
&lt;p&gt;As you can probably gather from the previous discussion, Blazor takes control of the entire GUI of the application and puts up a firm layer of abstraction over traditional native web technologies. All notion of JavaScript or code executing on a browser environment is greatly de-emphasized. In fact, most of Blazor executes in the server.&lt;/p&gt;
&lt;p&gt;Essentially, all the code that you actually write executes on the server side. When a user begins using the app, a two-way, persistent &lt;a href=&#34;https://dotnet.microsoft.com/en-us/apps/aspnet/signalr&#34;&gt;SignalR&lt;/a&gt; connection is established between the browser and server. Whenever the client requests a page, the server renders it and sends it through the connection to the client browser, where there&amp;rsquo;s a component that interprets and displays it. Likewise, whenever the user interacts with some UI element, like clicking a button or a link, the action is sent to the server via this SignalR connection for it to be processed. If changes to the GUI are necessary, the server calculates them and sends them back to the user&amp;rsquo;s browser, where they will be interpreted and used to re-render the screen with the new state.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/?view=aspnetcore-7.0#blazor-server&#34;&gt;Microsoft&amp;rsquo;s official documentation&lt;/a&gt; has an excellent diagram that helps explain the process:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/05/i-wrote-the-same-app-twice-with-hotwire-and-blazor-server/blazor-server.png&#34; alt=&#34;On the left, a cloud icon sits in front of a server icon. The cloud is labeled &amp;ldquo;ASP.NET Core&amp;rdquo;, and has two smaller boxes labeled &amp;ldquo;Razor Components&amp;rdquo; and &amp;ldquo;.NET&amp;rdquo;. There are two arrows pointing to and from a web browser diagram, with the arrows labeled &amp;ldquo;SignalR&amp;rdquo;. The web browser diagram has a smaller box in it labeled &amp;ldquo;DOM&amp;rdquo;.&#34;&gt;&lt;/p&gt;
&lt;p&gt;So, even though there is client side code running and browser DOM being manipulated, this is all happening under the hood. The developer doesn&amp;rsquo;t need to be concerned with that and can just focus on authoring C# code, for the most part.&lt;/p&gt;
&lt;p&gt;This approach has a few implications worth noting. One is that this means higher load on the server compared to more traditional web applications. This is mainly because there needs to be a connection always open between clients and the server, by design. Classic HTTP is purely stateless, and connections are typically opened and closed multiple times throughout a user&amp;rsquo;s session, as they interact with the web app. Not so for Blazor, where this SignalR connection (most likely via &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API&#34;&gt;WebSockets&lt;/a&gt;) is always alive. So, for scalability concerns, we need to keep in mind that the more concurrent users our app has, the more resources the server will consume, even if the users are somewhat idle.&lt;/p&gt;
&lt;h4 id=&#34;an-abstraction-over-the-requestresponse-cycle&#34;&gt;An abstraction over the request/response cycle&lt;/h4&gt;
&lt;p&gt;A key element of Blazor is that it abstracts developers from the classic &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview#http_messages&#34;&gt;HTTP request/​response model&lt;/a&gt;. With Blazor, one seldom has to consider that aspect, as all interactions between client and server are managed via the framework itself through the persistent connection we discussed in the last couple of paragraphs. To the developer, there&amp;rsquo;s no real separation between the two. This is a big departure from classic &lt;a href=&#34;https://dotnet.microsoft.com/en-us/apps/aspnet/mvc&#34;&gt;MVC&lt;/a&gt;-like frameworks where things like endpoints, actions, and controllers are front and center. Such concepts are simply not in play on Blazor apps. The idea is to make them feel more like desktop apps: fully integrated, monolithic, simple packages of GUI and functionality.&lt;/p&gt;
&lt;p&gt;This could become a double-edged sword as, in general, it is always important to have a clear understanding of the underlying technologies that support the application stack you&amp;rsquo;re working with. That is, the web is still there, even if you can&amp;rsquo;t see it. But as long as you&amp;rsquo;re cognizant of that, this approach can have great advantages too.&lt;/p&gt;
&lt;p&gt;For example, thanks to this approach, there&amp;rsquo;s no need to employ the classic &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Glossary/SPA&#34;&gt;SPA pattern&lt;/a&gt; of developing applications in two halves: 1. a backend Web API for domain logic written in some backend programming language, and 2. a frontend application written in JavaScript that implements the user experience and communicates with the backend over HTTP.&lt;/p&gt;
&lt;p&gt;Blazor attempts to offer a simpler solution from a developer&amp;rsquo;s perspective, where everything lives in the same execution environment so there&amp;rsquo;s no need for inter-process communication over HTTP. The example Blazor component from before demonstrates such a case. Notice how this link has an event handler registered to its click event:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;@&lt;/span&gt;&lt;span style=&#34;color:#369&#34;&gt;onclick&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;DeleteQuote&amp;#34;&lt;/span&gt;&amp;gt;Delete&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;a&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That &lt;code&gt;DeleteQuote&lt;/code&gt; event handler, simply defined as the method below, directly leverages the &lt;a href=&#34;https://learn.microsoft.com/en-us/ef/ef6/fundamentals/working-with-dbcontext&#34;&gt;&lt;code&gt;DbContext&lt;/code&gt;&lt;/a&gt; to issue the delete command to the underlying database:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; DeleteQuote()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;using&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;var&lt;/span&gt; context = dbContextFactory.CreateDbContext();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context.Quotes.Remove(QuoteToShow);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context.SaveChanges();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pretty simple.&lt;/p&gt;
&lt;p&gt;However, this flexibility also requires great discipline from the development team. It falls upon us not to clutter the GUI code with all manner of extraneous things like database calls and domain or application logic that have nothing to do with user interface concerns. The traditional two-halves pattern for SPAs has this separation between domain and GUI logic baked in by necessity. Blazor allows us to break free from it, but that does not mean that the separation is not useful or even necessary. For a small example like this, this works just fine, but for larger applications, a more modularized design should be considered as well. Maybe the introduction of abstractions like &lt;a href=&#34;https://martinfowler.com/eaaCatalog/repository.html&#34;&gt;repositories&lt;/a&gt; or &lt;a href=&#34;https://martinfowler.com/bliki/EvansClassification.html&#34;&gt;domain services&lt;/a&gt;? At the end of the day, the core software design principles still need to be applied.&lt;/p&gt;
&lt;h4 id=&#34;how-blazor-supports-common-frontend-framework-features&#34;&gt;How Blazor supports common frontend framework features&lt;/h4&gt;
&lt;p&gt;Something else to consider, which I touched on before, is that Blazor is built on top of .NET. That means that a solid understanding of .NET concepts is all but a necessity in order to be effective with Blazor. Most of the features that are now traditional and expected in frontend JavaScript frameworks exist in Blazor, and they are implemented using age-old .NET concepts. If your team has solid .NET experience, this is a blessing. If not, then Blazor requires a larger investment, one that could be overwhelming depending on your time constraints.&lt;/p&gt;
&lt;p&gt;Here are a few examples of how Blazor implements classic frontend framework features:&lt;/p&gt;
&lt;h5 id=&#34;handling-dom-events&#34;&gt;Handling DOM events&lt;/h5&gt;
&lt;p&gt;We already saw how &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-7.0&#34;&gt;event handlers&lt;/a&gt; work: they are defined as regular C# methods. The way they are registered to respond to events is via attributes in the GUI elements like &lt;code&gt;@onclick&lt;/code&gt; or &lt;code&gt;@onchange&lt;/code&gt;. All traditional DOM events are available. Like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;a&lt;/span&gt; &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;@&lt;/span&gt;&lt;span style=&#34;color:#369&#34;&gt;onclick&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;DeleteQuote&amp;#34;&lt;/span&gt;&amp;gt;Delete&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;a&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h5 id=&#34;including-other-blazor-components&#34;&gt;Including other Blazor components&lt;/h5&gt;
&lt;p&gt;We also saw how to render components from within other components. All it takes is adding the component to the template as if it was any other GUI element/HTML tag. The tag itself is the name of the component, which sometimes needs to be fully qualified. We saw an example before:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;QuoteEditorBlazor.Shared.Quotes.Edit&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#369&#34;&gt;QuoteToEdit&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;QuoteToShow&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#369&#34;&gt;OnCancel&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;HideEditForm&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The fully qualified component name is &lt;code&gt;QuoteEditorBlazor.Shared.Quotes.Edit&lt;/code&gt; in this case, and that&amp;rsquo;s how we reference it. If we were to include the namespace with an &lt;code&gt;@using&lt;/code&gt; statement (like &lt;code&gt;@using QuoteEditorBlazor.Shared.Quotes&lt;/code&gt; near the top of the file) then we would be able to invoke the component just by its name of &lt;code&gt;Edit&lt;/code&gt;.&lt;/p&gt;
&lt;h5 id=&#34;passing-parameters-to-components&#34;&gt;Passing parameters to components&lt;/h5&gt;
&lt;p&gt;That previous snippet also demonstrates how to pass parameters to components. In this case, we have two: &lt;code&gt;QuoteToEdit&lt;/code&gt; which is an object, and &lt;code&gt;OnCancel&lt;/code&gt; which is a custom event. As you can see, parameters are passed as if they were HTML element attributes. In the case of &lt;code&gt;QuoteToEdit&lt;/code&gt;, we&amp;rsquo;re passing it &lt;code&gt;QuoteToShow&lt;/code&gt;, which is a &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/properties&#34;&gt;C# Property&lt;/a&gt; defined in the &lt;code&gt;@code&lt;/code&gt; section of the component:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; QuoteEditorBlazor.Models.Quote QuoteToShow { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In order for the receiving component to be able to accept the parameter, it needs to define a property itself of the same type, annotated with the &lt;code&gt;Parameter&lt;/code&gt; &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/csharp/advanced-topics/reflection-and-attributes/&#34;&gt;attribute&lt;/a&gt;. In this case, the &lt;code&gt;QuoteEditorBlazor.Shared.Quotes.Edit&lt;/code&gt; component defines it like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;[Parameter]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; QuoteEditorBlazor.Models.Quote QuoteToEdit { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice how the name of the property is the same name used for the &amp;ldquo;HTML attribute&amp;rdquo; that was used in markup to pass the parameter to the component:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;QuoteToEdit=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;QuoteToShow&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In this case, the expected parameter is of type &lt;code&gt;QuoteEditorBlazor.Models.Quote&lt;/code&gt;.&lt;/p&gt;
&lt;h5 id=&#34;defining-handling-and-triggering-custom-component-events&#34;&gt;Defining, handling and triggering custom component events&lt;/h5&gt;
&lt;p&gt;The other parameter, which is a custom event, accepts a method. It looks like this in the receiving component:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;[Parameter]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; EventCallback OnCancel { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Again, this is a Property annotated with the &lt;code&gt;Parameter&lt;/code&gt; Attribute. The only difference is that the type of this one is &lt;code&gt;EventCallback&lt;/code&gt;. That&amp;rsquo;s what allows it to accept a method. Then, inside the receiving component, the event can be triggered with code like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;OnCancel.InvokeAsync();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will execute whatever method the parent component has registered as a handler for this custom event. In this case, that would be our &lt;code&gt;HideEditForm&lt;/code&gt; method.&lt;/p&gt;
&lt;h5 id=&#34;handling-component-lifecycle-events&#34;&gt;Handling component lifecycle events&lt;/h5&gt;
&lt;p&gt;Other than DOM and custom events, much like in other frontend frameworks, Blazor components also offer ways of hooking up to their own internal lifecycle events. &lt;code&gt;OnInitialized&lt;/code&gt; is one of the most important ones, which runs when the component is first starting up. To hook into it and run some code when it happens, all a Blazor component has to do is implement it as a &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/override&#34;&gt;method override&lt;/a&gt; within its code section. Something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; OnInitialized()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Do something here.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;rsquo;s more to learn about component lifecycle and &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-7.0&#34;&gt;the official documentation&lt;/a&gt; does a good job in explaining it.&lt;/p&gt;
&lt;h5 id=&#34;templates&#34;&gt;Templates&lt;/h5&gt;
&lt;p&gt;Like we discussed before, Blazor also offers a rich templating syntax via Razor, which is also a hallmark of modern frontend frameworks. We have conditional rendering, support for loops, interpolation of data, model binding, etc.&lt;/p&gt;
&lt;h5 id=&#34;routing&#34;&gt;Routing&lt;/h5&gt;
&lt;p&gt;Routing is also included in Blazor and the way that it works is with the &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-7.0#page&#34;&gt;&lt;code&gt;@page&lt;/code&gt;&lt;/a&gt; directive that&amp;rsquo;s used on top of components like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@page &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/quotes&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Not all components will use these, only those that correspond to whole pages. Most components will be used as portions of pages and as such, would not include these. A &lt;code&gt;@page&lt;/code&gt; statement like the above will make the component that includes it accessible via an URL like &lt;code&gt;http://mydomain.com/quotes&lt;/code&gt;. In other words, at the root of the site.&lt;/p&gt;
&lt;p&gt;These routes also support parameters, which can be defined like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@page &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/quotes/{quoteId:int}&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A component with this directive will be accessible through URLs like &lt;code&gt;http://mydomain.com/quotes/123&lt;/code&gt;. The code in the component can access the route parameter &lt;code&gt;quoteId&lt;/code&gt; by defining it as a Property with the matching type, annotated with the &lt;code&gt;Parameter&lt;/code&gt; attribute. Like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;[Parameter]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; QuoteId { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h5 id=&#34;state-management&#34;&gt;State management&lt;/h5&gt;
&lt;p&gt;Global application state management à la &lt;a href=&#34;https://vuex.vuejs.org/&#34;&gt;Vuex&lt;/a&gt; or &lt;a href=&#34;https://redux.js.org/&#34;&gt;Redux&lt;/a&gt; is also available in Blazor. The cool thing about how this is implemented in Blazor is that there is no need for any additional library or special components. A global app store can be a simple C# object that&amp;rsquo;s configured to have a lifetime that spans that of the user&amp;rsquo;s session. Here&amp;rsquo;s an example of a class that&amp;rsquo;s used to store global flash messages:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;QuoteEditorBlazor.AppState&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;FlashStore&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; List&amp;lt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt;&amp;gt; Messages { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; } = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; List&amp;lt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt;&amp;gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;event&lt;/span&gt; Action? MessagesChanged;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; AddMessage(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; message)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Messages.Add(message);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        MessagesChanged?.Invoke();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; Task.Delay(TimeSpan.FromSeconds(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt;));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Messages.Remove(message);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        MessagesChanged?.Invoke();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Like I said: a plain and simple C# class. It offers a method for displaying a message for a few seconds. It does this by storing the given message into an internal variable and then, after a little while, it gets removed. It also offers a &lt;code&gt;MessagesChanged&lt;/code&gt; &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/standard/events/&#34;&gt;event&lt;/a&gt; that other components can subscribe to which is triggered as messages are added and removed.&lt;/p&gt;
&lt;p&gt;The lifetime of the instance is controlled via its &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-7.0&#34;&gt;dependency injection&lt;/a&gt; configuration. In our &lt;code&gt;Program.cs&lt;/code&gt;, it would be configured like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;builder.Services.AddScoped&amp;lt;FlashStore&amp;gt;();&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Like I mentioned before, Blazor apps establish a persistent connection for the user throughout their session. This means that the instance of the app that is running on the server is also persistent. So, if we add &lt;code&gt;FlashStore&lt;/code&gt; as a &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#scoped&#34;&gt;scoped&lt;/a&gt; service, one single instance of it will also persist throughout the session. That&amp;rsquo;s why we can rely on its internal variables to store global application state.&lt;/p&gt;
&lt;p&gt;You can learn more about state management in Blazor &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/state-management?view=aspnetcore-7.0&amp;amp;pivots=server&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Then, you could have a component that renders those messages that looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@using QuoteEditorBlazor.AppState
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@inject FlashStore flashStore
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@implements IDisposable
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;div class=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;flash&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    @foreach (&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; message &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;in&lt;/span&gt; flashStore.Messages)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;div class=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;flash__message&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            @message
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;@code {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; OnInitialized()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        flashStore.MessagesChanged += StateHasChanged;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Dispose()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        flashStore.MessagesChanged -= StateHasChanged;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Very simple too. This Blazor component uses a loop to render all the messages. There are a couple of interesting things about this one. First, the component gets access to the instance of the &lt;code&gt;FlashStore&lt;/code&gt; class via the &lt;code&gt;@inject&lt;/code&gt; directive near the top of the file. That&amp;rsquo;s how the &lt;code&gt;flashStore&lt;/code&gt; variable is made available for the component to use both in C# code and in the template.&lt;/p&gt;
&lt;p&gt;The second interesting element is how this component registers its &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/components/lifecycle?view=aspnetcore-7.0#state-changes-statehaschanged&#34;&gt;&lt;code&gt;StateHasChanged&lt;/code&gt;&lt;/a&gt; method to the &lt;code&gt;MessagesChanged&lt;/code&gt; event defined in &lt;code&gt;FlashStore&lt;/code&gt;. As you recall, &lt;code&gt;FlashStore&lt;/code&gt; will trigger &lt;code&gt;MessagesChanged&lt;/code&gt; every time messages are added or removed. By registering &lt;code&gt;StateHasChanged&lt;/code&gt; to that event, we make sure that the component re-renders every time the messages list changes; which in turn ensures that the most current messages are always rendered. Kind of a neat trick.&lt;/p&gt;
&lt;p&gt;And as you might expect, any piece of code throughout the app can submit new messages to &lt;code&gt;FlashStore&lt;/code&gt; by getting hold of the global instance via dependency injection and then calling:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;flashStore.AddMessage(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Here&amp;#39;s a flash message!&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h5 id=&#34;interoperability-with-javascript&#34;&gt;Interoperability with JavaScript&lt;/h5&gt;
&lt;p&gt;For all the nice abstractions that Blazor presents us with, the reality is that sometimes we actually do have to break through them. In the case of JavaScript, this happens when, for example, we need access to some native browser feature, or when we need to integrate with some library for some widget or other type of capability. In Blazor, this is certainly possible.&lt;/p&gt;
&lt;p&gt;There are many details to consider when it comes to JavaScript interoperability in Blazor. Check out &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/?view=aspnetcore-7.0&#34;&gt;the official documentation&lt;/a&gt; to learn what all is possible. For our purposes here though, it&amp;rsquo;s enough to know that &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-javascript-from-dotnet?view=aspnetcore-7.0&#34;&gt;calls from .NET to JS&lt;/a&gt; and &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/blazor/javascript-interoperability/call-dotnet-from-javascript?view=aspnetcore-7.0&#34;&gt;vice versa&lt;/a&gt; are supported.&lt;/p&gt;
&lt;p&gt;The most basic example of calling JS code from .NET code looks like the following. If we have a JS function like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;window&lt;/span&gt;.showMessagefromServer = (message) =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    alert(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;`The server says: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;${&lt;/span&gt;message&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;The client says thanks!&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Such a method can be invoked from a Blazor component like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; interopResult = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; js.InvokeAsync&amp;lt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt;&amp;gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;showMessagefromServer&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Have a nice day!&amp;#34;&lt;/span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Blazor component that runs this code would have to inject an instance of &lt;code&gt;IJSRuntime&lt;/code&gt; into the &lt;code&gt;js&lt;/code&gt; variable with a statement like &lt;code&gt;@inject IJSRuntime js&lt;/code&gt;. A key thing to note is that the JavaScript method needs to be attached to the &lt;code&gt;window&lt;/code&gt; object in order for it to be accessible.&lt;/p&gt;
&lt;p&gt;Like I said, the other way around also works: JavaScript code is able to call .NET logic defined in a server-side Blazor component. Here&amp;rsquo;s a simple example for us to get an idea of how it feels. We can have a method that looks like this on a Blazor component:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;[JSInvokable]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;static&lt;/span&gt; Task&amp;lt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt;&amp;gt; getMessageFromServer()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Task.FromResult(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Have a nice day!&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This method is public and &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-classes-and-static-class-members&#34;&gt;static&lt;/a&gt;. It is also possible to invoke non-static instance methods, but the setup is a little more complicated, and this is enough for our purposes here. Other than that, as you can see, the method needs to be annotated with the &lt;code&gt;JSInvokable&lt;/code&gt; attribute and return a type of &lt;code&gt;Task&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s some JavaScript that calls this method:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-js&#34; data-lang=&#34;js&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;window&lt;/span&gt;.returnArrayAsync = () =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    DotNet.invokeMethodAsync(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;{YOUR APP ASSEMBLY NAME}&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;getMessageFromServer&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        .then(data =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            alert(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;`I asked the server and it says: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;${&lt;/span&gt;data&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;`&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;};&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are a few noteworthy things here. First we have the &lt;code&gt;DotNet.invokeMethodAsync&lt;/code&gt; function which Blazor makes available to us in JavaScript land. That&amp;rsquo;s what we use to invoke .NET server side code. Next, the function needs to be given the &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/standard/assembly/&#34;&gt;assembly&lt;/a&gt; name of our app as well as the name of the method to invoke within the Blazor component. This is the one that was annotated with &lt;code&gt;JSInvokable&lt;/code&gt;. Finally, the function itself is asynchronous and thus returns a &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise&#34;&gt;promise&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;an-overview-of-hotwire&#34;&gt;An overview of Hotwire&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://hotwired.dev/&#34;&gt;Hotwire&lt;/a&gt; makes a promise similar to Blazor&amp;rsquo;s. However, the way it goes about it couldn&amp;rsquo;t be more different.&lt;/p&gt;
&lt;p&gt;Hotwire is much simpler and more minimalistic than Blazor. Whereas in Blazor we let the framework take total control of the GUI, just like we do with other popular frontend frameworks, Hotwire feels more like a natural evolution of traditional pre-JavaScript-heavy web development. We still have &lt;a href=&#34;https://guides.rubyonrails.org/action_controller_overview.html&#34;&gt;controllers&lt;/a&gt; and &lt;a href=&#34;https://guides.rubyonrails.org/action_view_overview.html&#34;&gt;views&lt;/a&gt;, we still render pages on the server, and we still work in tandem with HTTP&amp;rsquo;s request/​response model.&lt;/p&gt;
&lt;p&gt;What Hotwire gives us, if we were to boil it down to a single sentence, is a way to refresh only portions of our pages as a result of user interactions. That is, we&amp;rsquo;re not forced to reload the entire page, as is the case with non-JavaScript web applications. For example, in Hotwire we have the ability to submit a form or click a link and, as a result of that, only update a particular message, picture, or section.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;ASP.NET veterans will find this awfully familiar. That&amp;rsquo;s because all the way back in version 3.5, &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/web-forms/overview/older-versions-getting-started/aspnet-ajax/&#34;&gt;ASP.NET AJAX&lt;/a&gt; offered a very similar feature: partial page updates with the UpdatePanel component. Indeed, Hotwire presents a very similar concept; only greatly improved and modernized.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;When it comes to actual coding, Hotwire&amp;rsquo;s footprint is minimal. It augments traditional Ruby on Rails controllers and views to produce the desired effect of page refreshes that are partial and targeted. Let&amp;rsquo;s walk through an example and you&amp;rsquo;ll see how we can start with a regular looking Rails app and then, through minor adjustments, we end up with a more richly interactive experience.&lt;/p&gt;
&lt;p&gt;Imagine we are beginning to develop support for CRUDing a particular type of record called &amp;ldquo;Quote&amp;rdquo;, and we have these files:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;### app/controllers/quotes_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;QuotesController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;index&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;new&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#33b&#34;&gt;@quote&lt;/span&gt; = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Quote&lt;/span&gt;.new
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- app/views/quotes/index.html.erb --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;main&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;header&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt;&amp;gt;Quotes&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= link_to &amp;#34;New quote&amp;#34;, new_quote_path %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;main&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- app/views/quotes/new.html.erb --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;main&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;container&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= link_to &amp;#34;Back to quotes&amp;#34;, quotes_path %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;header&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt;&amp;gt;New quote&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;h1&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= render &amp;#34;form&amp;#34;, quote: @quote %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;main&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- app/views/quotes/_form.html.erb --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= simple_form_for quote do |f| %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;% if quote.errors.any? %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;error-message&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= quote.errors.full_messages.to_sentence.capitalize %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;% end %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= f.input :name %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= f.submit %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;% end %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;This sample is using the &lt;code&gt;simple_form&lt;/code&gt; gem, which you can learn more about &lt;a href=&#34;https://github.com/heartcombo/simple_form&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;If you&amp;rsquo;re familiar with Rails, then you understand what&amp;rsquo;s happening here. We have a simple &lt;code&gt;index&lt;/code&gt; page with a heading and a link to another page. That other page contains a form to create new Quote records. It also contains a link to go back to the &lt;code&gt;index&lt;/code&gt; page.&lt;/p&gt;
&lt;h4 id=&#34;partial-page-updates-with-turbo-frames&#34;&gt;Partial page updates with Turbo Frames&lt;/h4&gt;
&lt;p&gt;As they are right now, these files would produce a traditional web application user experience. When links are clicked, the whole screen will be reloaded to show the page that the clicked link points to. But what if we wanted, for example, to have the new record creation form appear out of nowhere within the same &lt;code&gt;index&lt;/code&gt; page, without a full page reload? Here&amp;rsquo;s what that would look like with Hotwire:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;!-- app/views/quotes/index.html.erb --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;main class=&amp;#34;container&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;div class=&amp;#34;header&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;h1&amp;gt;Quotes&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;%= link_to &amp;#34;New quote&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 new_quote_path,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+                data: { turbo_frame: dom_id(Quote.new) } %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;   &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  &amp;lt;%= turbo_frame_tag Quote.new %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; &amp;lt;/main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;!-- app/views/quotes/new.html.erb --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;main class=&amp;#34;container&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;%= link_to sanitize(&amp;#34;&amp;amp;larr; Back to quotes&amp;#34;), quotes_path %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;div class=&amp;#34;header&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;h1&amp;gt;New quote&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  &amp;lt;%= turbo_frame_tag Quote.new do %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;     &amp;lt;%= render &amp;#34;form&amp;#34;, quote: @quote %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  &amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; &amp;lt;/main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Remember that these examples are taken from a fully working application. Feel free to read through &lt;a href=&#34;https://github.com/megakevin/quote-editor-hotwire&#34;&gt;the source code&lt;/a&gt; to have a more complete understanding of the context within which these files exist.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;And that&amp;rsquo;s really all it takes. Let&amp;rsquo;s go over it. With these changes, whenever a user clicks on the &amp;ldquo;New quote&amp;rdquo; link, instead of the browser triggering the usual GET request to then reload the screen and show the creation page, Hotwire&amp;rsquo;s frontend component captures the click event and makes the request itself via &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest&#34;&gt;XHR&lt;/a&gt;. The server then receives this request normally and routes it to the &lt;code&gt;new&lt;/code&gt; action in the &lt;code&gt;quotes&lt;/code&gt; controller. All that action does is render the &lt;code&gt;new.html.erb&lt;/code&gt; template and send that back to the client as a response.&lt;/p&gt;
&lt;p&gt;When the Hotwire frontend component receives this, it notices that some of the response is enclosed in a &lt;code&gt;turbo_frame_tag&lt;/code&gt; whose ID is that of an empty new Quote object. Now, because the link that the user clicked also had the ID of an empty new Quote object as its &lt;code&gt;data-turbo_frame&lt;/code&gt; attribute, Hotwire looks for a &lt;code&gt;turbo_frame_tag&lt;/code&gt; with the same ID in the page that&amp;rsquo;s currently being shown an replaces its contents with the contents from the similarly named &lt;code&gt;turbo_frame_tag&lt;/code&gt; from the incoming response.&lt;/p&gt;
&lt;p&gt;With that, you&amp;rsquo;ve seen in action some of the key elements that make Hotwire work. First of all we have the &lt;code&gt;turbo_frame_tag&lt;/code&gt; helper, which produces a so-called &amp;ldquo;&lt;a href=&#34;https://turbo.hotwired.dev/handbook/frames&#34;&gt;Turbo Frame&lt;/a&gt;&amp;rdquo;. Turbo Frames are the main building block that we use for partial page updates. Turbo Frames essentially say: &amp;ldquo;This section of the page is allowed to be dynamically updated without a full page refresh.&amp;rdquo; In an app that uses Hotwire, whenever you&amp;rsquo;re in a page that includes a Turbo Frame, if a request is made (whether it be navigation or form submission), and if the resulting response includes within it another Turbo Frame with the same ID, then Hotwire will notice the match and update the current page&amp;rsquo;s Turbo Frame with the contents of the Turbo Frame from the response.&lt;/p&gt;
&lt;p&gt;Looking back at the code, you can see how we achieved this. We added an empty Turbo Frame on the index page, right below the link to the creation page. We also wrapped the form from the creation page in a similarly named Turbo Frame. Finally, we added the &lt;code&gt;data-turbo_frame&lt;/code&gt; attribute to the link in the index page to tell Hotwire that it should kick in for this link and target that specific Turbo Frame. If the link was inside the Turbo Frame, we would not have to do this. Since it is outside, Hotwire needs the little hint that says: &amp;ldquo;Treat this link as if it was inside this Turbo Frame.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I feel like this is at the same time a little awkward to wrap your head around and deceptively simple. When compared to Blazor, which builds upon tried and true concepts (as far as the developer experience goes at least), Hotwire almost seems alien, with a much more unusual style. But all in all, one can&amp;rsquo;t deny just how clean and simple all of this looks, in that &amp;ldquo;Rails magic&amp;rdquo; kind of way. And once it clicks, you can begin to see a world of possibilities opening up. The Hotwire developers managed to identify and extract a general design pattern of web application interactions, one that can be leveraged to produce a lot of varying rich interactive user experiences.&lt;/p&gt;
&lt;p&gt;One neat aspect worth noting is that, if the user were to disable JavaScript, the app would still fully work. It would &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Glossary/Graceful_degradation&#34;&gt;gracefully degrade&lt;/a&gt;. This is a direct consequence of Hotwire&amp;rsquo;s paradigm of adding minimal features on top of the existing traditional Rails programming model.&lt;/p&gt;
&lt;h4 id=&#34;imperative-rendering-with-turbo-streams&#34;&gt;Imperative rendering with Turbo Streams&lt;/h4&gt;
&lt;p&gt;Let&amp;rsquo;s consider another example now. This time we&amp;rsquo;ll see another key component of Hotwire in action which is called &lt;a href=&#34;https://turbo.hotwired.dev/handbook/streams&#34;&gt;Turbo Streams&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Turbo Streams helps solve the problem that emerges when the declarative style of pure Turbo Frames is not enough to obtain the fine grained control and behavior that we need. In these cases, the server needs to issue specific commands to the frontend on how to update the GUI. Via Turbo Streams, we can achieve just that. Coding-wise, these look like regular Rails view templates, albeit with some funky syntax thanks to the Turbo Stream helpers.&lt;/p&gt;
&lt;p&gt;In our example, we&amp;rsquo;ll add a list of quotes in the index page, right below the link and the form. We&amp;rsquo;ll also complete our quote creation implementation so that we&amp;rsquo;re actually able to submit the form. As a result of that, we want the GUI to be updated so that the form goes away and the newly created quote is shown in the list. These last two are the things that we&amp;rsquo;ll use Turbo Streams for. Of course, these updates to the page will be done without a full page refresh.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s start by adding a list of quotes on the index page.&lt;/p&gt;
&lt;p&gt;In the controller, we query the database for all the quote records and store them in a variable that the view can later access.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ### app/controllers/quotes_controller.rb
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; class QuotesController &amp;lt; ApplicationController
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   def index
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    @quotes = Quote.all
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;   end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   def new
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     @quote = Quote.new
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; end
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In the index view template, we render the collection of records.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;!-- app/views/quotes/index.html.erb --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;main class=&amp;#34;container&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;div class=&amp;#34;header&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;h1&amp;gt;Quotes&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;%= link_to &amp;#34;New quote&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 new_quote_path,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 data: { turbo_frame: dom_id(Quote.new) } %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;%= turbo_frame_tag Quote.new %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  &amp;lt;%= render @quotes %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; &amp;lt;/main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we need to define a &amp;ldquo;_quote&amp;rdquo; partial view so that render statement we added on the index view can work &lt;a href=&#34;https://thesaurus.plus/img/synonyms/128/automagically.png&#34;&gt;automagically&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- app/views/quotes/_quote.html.erb --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;quote&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= quote.name %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;div&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this, the &lt;code&gt;render @quotes&lt;/code&gt; statement will loop through all the records in &lt;code&gt;@quotes&lt;/code&gt; and render this partial view for each on of them. This template is very simple. All it does is render the name of the quote.&lt;/p&gt;
&lt;p&gt;Now let&amp;rsquo;s add an action that can accept quote creation form submissions:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ### app/controllers/quotes_controller.rb
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; class QuotesController &amp;lt; ApplicationController
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   def index
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     @quotes = Quote.all
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   def new
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     @quote = Quote.new
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  def create
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    @quote = Quote.new(quote_params)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    if @quote.save
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+      redirect_to quotes_path, notice: &amp;#34;Quote was successfully created.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    else
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+      render :new
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  def quote_params
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    params.require(:quote).permit(:name)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; end
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A typical Rails recipe for a record creation endpoint. It takes the parameters coming from the request and uses them to create a new record via the &lt;code&gt;Quote&lt;/code&gt; &lt;a href=&#34;https://guides.rubyonrails.org/active_record_basics.html&#34;&gt;Active Record&lt;/a&gt; model. If successful, it redirects to the index page; if not, it renders the creation page again (via the &lt;code&gt;new&lt;/code&gt; action). The &lt;code&gt;new.html.erb&lt;/code&gt; view template has some logic to render error messages when they are present so those are going to show up when that page is rendered as a result of unsuccessful calls to this &lt;code&gt;create&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;p&gt;At this point, we&amp;rsquo;re able to view all the quotes on record and create new ones. Now here are the changes to Hotwire-ify this scenario.&lt;/p&gt;
&lt;p&gt;First we wrap the list of quotes with a Turbo Frame named &amp;ldquo;&lt;code&gt;quotes&lt;/code&gt;&amp;rdquo;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;!-- app/views/quotes/index.html.erb --&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &amp;lt;main class=&amp;#34;container&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;div class=&amp;#34;header&amp;#34;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;h1&amp;gt;Quotes&amp;lt;/h1&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     &amp;lt;%= link_to &amp;#34;New quote&amp;#34;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 new_quote_path,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 data: { turbo_frame: dom_id(Quote.new) } %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;/div&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   &amp;lt;%= turbo_frame_tag Quote.new %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  &amp;lt;%= turbo_frame_tag &amp;#34;quotes&amp;#34; do %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;     &amp;lt;%= render @quotes %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  &amp;lt;% end %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; &amp;lt;/main&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, we employ Turbo Streams. Like I said, Turbo Streams materialize themselves in code as if they were view templates. So, a new file is added that looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- app/views/quotes/create.turbo_stream.erb --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= turbo_stream.prepend &amp;#34;quotes&amp;#34;, partial: &amp;#34;quotes/quote&amp;#34;, locals: { quote: @quote } %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= turbo_stream.update Quote.new, &amp;#34;&amp;#34; %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It sort of looks like a couple of imperative statements, does it not? The first line instructs Hotwire to prepend, in the &lt;code&gt;&amp;quot;quotes&amp;quot;&lt;/code&gt; Turbo Frame, a new render of the &lt;code&gt;app/views/quotes/_quote.html.erb&lt;/code&gt; partial view, while passing it the &lt;code&gt;@quote&lt;/code&gt; object as a parameter. The second line updates the &lt;code&gt;Quote.new&lt;/code&gt; Turbo Frame to be empty. When we remember that the &lt;code&gt;&amp;quot;quotes&amp;quot;&lt;/code&gt; Turbo Frame is the one that contains the list of records and the &lt;code&gt;Quote.new&lt;/code&gt; Turbo Frame is the one that contains the new quote creation form, this starts to make sense. This Turbo Streams view is making the newly created quote appear in the list; and at the same time, it is making the form disappear. From a user&amp;rsquo;s perspective, this all takes place after submitting the creation form. So the user experience makes complete sense. All that with no full page refresh.&lt;/p&gt;
&lt;p&gt;And finally, the controller action needs to make use of this new view template like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ### app/controllers/quotes_controller.rb
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; def create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   @quote = Quote.new(quote_params)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   if @quote.save
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#fdd&#34;&gt;-    redirect_to quotes_path, notice: &amp;#34;Quote was successfully created.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#fdd&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    respond_to do |format|
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+      format.html { redirect_to quotes_path, notice: &amp;#34;Quote was successfully created.&amp;#34; }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+      format.turbo_stream
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;   else
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     render :new, status: :unprocessable_entity
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; end
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is yet another familiar Rails pattern. This is how we specify different &lt;a href=&#34;https://guides.rubyonrails.org/action_controller_overview.html#rendering-xml-and-json-data&#34;&gt;response formats&lt;/a&gt;, whether it be HTML, JSON, XML. Now, thanks to Hotwire, we can also specify Turbo Streams. This is one of the great aspects about Hotwire: it seamlessly integrates with Rails&amp;rsquo; existing features and concepts.&lt;/p&gt;
&lt;p&gt;Anyway, in this case, we invoke &lt;code&gt;format.turbo_stream&lt;/code&gt; within the block passed to &lt;code&gt;respond_to&lt;/code&gt; and that makes it so the &lt;code&gt;app/views/quotes/create.turbo_stream.erb&lt;/code&gt; view template is included in this action&amp;rsquo;s response. Hotwire&amp;rsquo;s frontend component sees this coming as part of the response and acts accordingly, updating the GUI how it&amp;rsquo;s been specified.&lt;/p&gt;
&lt;h4 id=&#34;adding-javascript-with-stimulus&#34;&gt;Adding JavaScript with Stimulus&lt;/h4&gt;
&lt;p&gt;The final piece of the Hotwire puzzle is &lt;a href=&#34;https://stimulus.hotwired.dev/&#34;&gt;Stimulus&lt;/a&gt;, which allows us to integrate JavaScript functionality in a neat way. Stimulus is a very simple JavaScript framework, it does not take control of the entire UI. In fact, it does not render any HTML at all. Stimulus essentially offers a nice way of wiring up JS behavior to existing markup. Let&amp;rsquo;s look at a quick example of how it could hypothetically be used for showing a confirmation popup before deleting a record.&lt;/p&gt;
&lt;p&gt;The JavaScript logic lives inside so-called &amp;ldquo;Stimulus controllers&amp;rdquo;. For our example, it could look something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// app/javascript/controllers/confirmations_controller.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { Controller } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;@hotwired/stimulus&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;default&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;extends&lt;/span&gt; Controller {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  confirm() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (confirm(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Are you sure you want to delete this record?&amp;#34;&lt;/span&gt;)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;// Carry on with the operation...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And now, the idea is to wire up this code so that it runs when the hypothetical delete button is clicked. Here&amp;rsquo;s what that button could like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;button&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#369&#34;&gt;data-controller&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;confirmations&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#369&#34;&gt;data-action&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;click-&amp;gt;confirmations#confirm&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  Delete
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;button&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pretty self-explanatory. We specify the name of the controller to use via the &lt;code&gt;data-controller&lt;/code&gt; attribute. We also specify, via &lt;code&gt;data-action&lt;/code&gt;, what method to invoke within that controller as a result of which DOM event, &lt;code&gt;click&lt;/code&gt; in this case.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s basically all it takes to sprinkle some JavaScript on Hotwire apps. Stimulus does offer a few more useful features, which you can read more about in &lt;a href=&#34;https://stimulus.hotwired.dev/handbook/introduction&#34;&gt;the official documentation&lt;/a&gt;. But for us for now, it&amp;rsquo;s enough to know that it exists, and what&amp;rsquo;s the main idea behind it.&lt;/p&gt;
&lt;p&gt;So as you can hopefully see, Hotwire is much smaller in scope to Blazor. And yet, it allows us to do just the same: highly interactive applications with little to no JavaScript. This ought to make a lot of Rails developers happy.&lt;/p&gt;
&lt;h3 id=&#34;a-final-comparison&#34;&gt;A final comparison&lt;/h3&gt;
&lt;p&gt;I was initially thinking about ending this blog post with a flowchart of sorts to explain the process of deciding which of these two technologies you should use. But really, the decision is very simple: If your team is comfortable with .NET, use Blazor. If your team is comfortable with Ruby on Rails, use Hotwire. It&amp;rsquo;s obvious, so I won&amp;rsquo;t claim to have made a great discovery here.&lt;/p&gt;
&lt;p&gt;The only thing to add is that if your team is familiar with modern frontend web development framework concepts, you&amp;rsquo;ll be even better served by Blazor and you&amp;rsquo;ll hit the ground running. If not, then even for seasoned .NET people, there will be a decent learning curve, but not steep enough to be deterred. Moreover, if your team has no modern frontend development experience at all, then Hotwire is a godsend; thanks to its &amp;ldquo;augment classic backend-heavy web app development&amp;rdquo; style.&lt;/p&gt;
&lt;p&gt;With that said, let&amp;rsquo;s close out with a summary of main aspects of both technologies and how they compare to each other.&lt;/p&gt;
&lt;p&gt;Overall, Hotwire is much simpler than Blazor. While Blazor is a full-fledged GUI component framework, Hotwire&amp;rsquo;s approach is more like an augmentation of classic non-JavaScript web development patterns. That said, Hotwire&amp;rsquo;s style is more unusual than Blazor&amp;rsquo;s, so if your team is already familiar with modern frontend web development, Blazor can be a great fit.&lt;/p&gt;
&lt;p&gt;While both frameworks try to offer enough functionality to allow the development of rich interactive experiences without the need to write any JavaScript, the reality is that sometimes JavaScript does need to be written. Both technologies offer ways to make this happen. And while both are perfectly workable, Blazor&amp;rsquo;s solution is a bit more clunky than Hotwire&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;When it comes to classic web technologies like HTTP&amp;rsquo;s request/​response cycle and the separation between server and client, Blazor&amp;rsquo;s style offers a big deviation from them. It greatly de-emphasizes them and presents instead a completely different programming model, one more akin to desktop application development. The concepts of request, response, client, and server seem to vanish. Not so for Hotwire, which builds upon these classic technologies in a way where they still need to be considered and are in fact in the spotlight. While Blazor attempts to do away with these, Hotwire embraces them.&lt;/p&gt;
&lt;p&gt;In Blazor, client events are sent to the server, the server renders the DOM updates and sends them to the client for updates. This happens via the persistent SignalR/​WebSockets connection.&lt;/p&gt;
&lt;p&gt;Hotwire, on the other hand, intercepts client events and sends classic HTTP requests (via AJAX/XHR) to the server. The server then executes the requests and sends back the responses to the client which carries out the necessary operations, generally speaking, updating sections of the page that&amp;rsquo;s already being displayed.&lt;/p&gt;
&lt;p&gt;That means that at the end of the day, both frameworks do the rendering on the server side and send the rendered markup over the wire to the clients. But in Blazor, the client and server have a persistent connection, while Hotwire&amp;rsquo;s connections come and go as normal HTTP requests and responses.&lt;/p&gt;
&lt;p&gt;A neat aspect of Hotwire&amp;rsquo;s programming model is that it allows an incremental approach to web development where you can start developing the app like you would a traditional, non-reactive, non-JS app, then augment it with a little code to give it SPA capabilities.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s all for now! I for one am glad to see these types of technologies emerge. While there are many teams out there that are already effective and productive with the current landscape of frontend web development, these two are very interesting and seem capable in their own right.&lt;/p&gt;
&lt;p&gt;Besides, having alternatives is never a bad thing. Depending mainly on your previous experience, these could be a great fit for projects new and old. It&amp;rsquo;s great to know that both .NET and Rails include these types of offerings and that they work pretty well.&lt;/p&gt;
&lt;h3 id=&#34;table-of-contents&#34;&gt;Table of contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#what-this-article-is&#34;&gt;What this article is&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#an-overview-of-blazor&#34;&gt;An overview of Blazor&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#how-blazor-works-under-the-hood&#34;&gt;How Blazor works under the hood&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#an-abstraction-over-the-requestresponse-cycle&#34;&gt;An abstraction over the request/response cycle&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#how-blazor-supports-common-frontend-framework-features&#34;&gt;How Blazor supports common frontend framework features&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#handling-dom-events&#34;&gt;Handling DOM events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#including-other-blazor-components&#34;&gt;Including other Blazor components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#passing-parameters-to-components&#34;&gt;Passing parameters to components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#defining-handling-and-triggering-custom-component-events&#34;&gt;Defining, handling and triggering custom component events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#handling-component-lifecycle-events&#34;&gt;Handling component lifecycle events&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#templates&#34;&gt;Templates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#routing&#34;&gt;Routing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#state-management&#34;&gt;State management&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#interoperability-with-javascript&#34;&gt;Interoperability with JavaScript&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#an-overview-of-hotwire&#34;&gt;An overview of Hotwire&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#partial-page-updates-with-turbo-frames&#34;&gt;Partial page updates with Turbo Frames&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#imperative-rendering-with-turbo-streams&#34;&gt;Imperative rendering with Turbo Streams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#adding-javascript-with-stimulus&#34;&gt;Adding JavaScript with Stimulus&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#a-final-comparison&#34;&gt;A final comparison&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

      </content>
    </entry>
  
    <entry>
      <title>Implementing Basic HTTP Authentication in Rails</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/01/implementing-basic-http-authentication-in-rails/"/>
      <id>https://www.endpointdev.com/blog/2023/01/implementing-basic-http-authentication-in-rails/</id>
      <published>2023-01-26T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/01/implementing-basic-http-authentication-in-rails/deer-on-hill.webp&#34; alt=&#34;Two deer stand on a steep mountain slope. The mountain is reddened by the sunset, and cuts the image in half diagonally, with the other half being dominated by a pale blue sky. In the bottom, behind the front slope, lies a tall, snow-covered peak.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2022 --&gt;
&lt;p&gt;Nowadays it&amp;rsquo;s rather unusual to deploy &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication&#34;&gt;HTTP Basic Authentication&lt;/a&gt; in a production web application. However, the need came up recently from a client. In a nutshell, due to integration requirements with a third party system, we had to provide a web app which expected credentials supplied via Basic HTTP Auth and validated against an external web service.&lt;/p&gt;
&lt;p&gt;Luckily for us, like a great many other things, this is very easy to implement with &lt;a href=&#34;https://rubyonrails.org/&#34;&gt;Ruby on Rails&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;setting-up-a-new-rails-project&#34;&gt;Setting up a new Rails project&lt;/h3&gt;
&lt;p&gt;If you want to work along with me, and you like &lt;a href=&#34;https://www.docker.com/&#34;&gt;Docker&lt;/a&gt; and &lt;a href=&#34;https://code.visualstudio.com/&#34;&gt;VS Code&lt;/a&gt;, take a look at &lt;a href=&#34;/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/&#34;&gt;this blog post&lt;/a&gt; to learn about the easiest way to set up an environment for development with Ruby on Rails in a container.&lt;/p&gt;
&lt;p&gt;If not, you can follow &lt;a href=&#34;https://www.ruby-lang.org/en/documentation/installation/&#34;&gt;the official docs&lt;/a&gt; for installing Ruby.&lt;/p&gt;
&lt;p&gt;Once you have your environment with Ruby ready, we can go ahead and create a new Rails project to demonstrate how to set up Basic HTTP Auth.&lt;/p&gt;
&lt;h4 id=&#34;creating-the-new-project&#34;&gt;Creating the new project&lt;/h4&gt;
&lt;p&gt;First, install the rails gem:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ gem install rails&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, make sure you are located in the directory where you want to create the new project and do:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ rails new . --minimal -O&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;--minimal&lt;/code&gt; is a new option to &lt;code&gt;rails new&lt;/code&gt; added in version 6.1 that disables a lot of default features and gems. You can learn more about it &lt;a href=&#34;https://github.com/rails/rails/pull/39282&#34;&gt;here&lt;/a&gt;. &lt;code&gt;-O&lt;/code&gt; excludes ActiveRecord from the project. We will not be using databases for this quick app so we don&amp;rsquo;t need it.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;And finally, run the app:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Open a browser and navigate to &lt;a href=&#34;http://127.0.0.1:3000/&#34;&gt;http://127.0.0.1:3000/&lt;/a&gt; to see the classic Rails hello world screen:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/implementing-basic-http-authentication-in-rails/hello-rails.png&#34; alt=&#34;Hello Rails. A browser navigated to http://127.0.0.1:3000/, with the webpage displaying the Rails logo, and underneath reading &amp;ldquo;Rails version: 7.0.4&amp;rdquo;; &amp;ldquo;Ruby version: ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e)[x86_64-linux]&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;implementing-basic-http-auth&#34;&gt;Implementing Basic HTTP Auth&lt;/h3&gt;
&lt;p&gt;Now that we have a Rails app up and running, let&amp;rsquo;s actually add Basic HTTP auth to it.&lt;/p&gt;
&lt;p&gt;First of all we need a page that we will secure:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails generate controller Pages home --no-helper&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That&amp;rsquo;ll give us a &amp;ldquo;Pages&amp;rdquo; controller with a &amp;ldquo;home&amp;rdquo; action and its corresponding view.&lt;/p&gt;
&lt;p&gt;Next, we replace the &lt;code&gt;get &#39;pages/home&#39;&lt;/code&gt; line in &lt;code&gt;routes.rb&lt;/code&gt; with &lt;code&gt;root &#39;pages#home&#39;&lt;/code&gt;. That will make it so the root URL of our app points to the action created in the previous step.&lt;/p&gt;
&lt;p&gt;Going back to the browser at &lt;a href=&#34;http://127.0.0.1:3000/&#34;&gt;http://127.0.0.1:3000/&lt;/a&gt; you should see this now:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/implementing-basic-http-authentication-in-rails/homepage.png&#34; alt=&#34;A browser navigated to http://127.0.0.1:3000/. On the page, a header reads &amp;ldquo;Pages#home&amp;rdquo;, with a sentence below reading &amp;ldquo;Find me in app/views/pages/home.html.erb&amp;rdquo;.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Now we can restrict access to this page with Basic HTTP Auth by adding this code to the &amp;ldquo;PagesController&amp;rdquo;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; class PagesController &amp;lt; ApplicationController
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  before_action :authenticate_http_basic
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   def home
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  def authenticate_http_basic
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    return if authenticate_with_http_basic { |un, pw| do_authentication(un, pw) }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    request_http_basic_authentication
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  def do_authentication(username, password)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+    username == &amp;#34;username&amp;#34; &amp;amp;&amp;amp; password == &amp;#34;password&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  end
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; end
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And that&amp;rsquo;s really all it takes. Try to navigate to &lt;a href=&#34;http://127.0.0.1:3000/&#34;&gt;http://127.0.0.1:3000/&lt;/a&gt; in the browser and you&amp;rsquo;ll now encounter this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/implementing-basic-http-authentication-in-rails/basic-http-auth-popup.png&#34; alt=&#34;A popup is shown above the home page, showing the site which created it (http://127.0.0.1:3000), and reading &amp;ldquo;This site is asking you to sign in&amp;rdquo;, followed by text inputs for Username and Password. At the bottom of the popup are buttons for Cancel and Sign in.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Basic HTTP Auth in action! Hit cancel or input the wrong credentials and a &amp;ldquo;HTTP Basic: Access denied.&amp;rdquo; message will be shown; type in &amp;ldquo;username&amp;rdquo; and &amp;ldquo;password&amp;rdquo; and our beautiful homepage shows up.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s now discuss the few lines of code that we added to the controller.&lt;/p&gt;
&lt;p&gt;First is the &lt;code&gt;authenticate_http_basic&lt;/code&gt; method which we&amp;rsquo;ve registered to the controller&amp;rsquo;s &lt;code&gt;before_action&lt;/code&gt; callback. That means that before processing any of the controller&amp;rsquo;s actions (&lt;code&gt;home&lt;/code&gt; being the only one), the method will be invoked and serve as a precondition for its execution.&lt;/p&gt;
&lt;p&gt;In the method, the first thing we do is call &lt;code&gt;authenticate_with_http_basic&lt;/code&gt;, which is how we tell Rails to authenticate the current request using Basic HTTP Auth. The block that we pass to the method is provided the username and password that the user typed in and is where we put any custom logic that we may have for validating the credentials.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;authenticate_with_http_basic&lt;/code&gt; returns whatever the block returns. That&amp;rsquo;s why we have that &lt;code&gt;return if ...&lt;/code&gt; guard clause as the first line of our &lt;code&gt;authenticate_http_basic&lt;/code&gt; method. If the block returns truthy, the auth is deemed successful and the user is allowed in the page; if it returns falsy, execution continues and reaches the &lt;code&gt;request_http_basic_authentication&lt;/code&gt; line; which prompts the authentication popup form that we saw earlier.&lt;/p&gt;
&lt;p&gt;In our case, the custom logic that validates the credentials is very simple. It just calls a &lt;code&gt;do_authentication&lt;/code&gt; method that checks the inputs against hardcoded values. But in the real world, this can be anything. For the client that I discussed at the beginning to this article, the requirement was to invoke an external web service to validate the provided credentials. You could also check the credentials against some database or file, etc.&lt;/p&gt;
&lt;h4 id=&#34;encapsulating-the-basic-http-auth-logic-in-a-module&#34;&gt;Encapsulating the Basic HTTP Auth logic in a module&lt;/h4&gt;
&lt;p&gt;I also thought it&amp;rsquo;d be nice to encapsulate this in a module for easier reuse. That&amp;rsquo;s something that&amp;rsquo;s easy to accomplish thanks to &lt;a href=&#34;https://api.rubyonrails.org/v7.0.4/classes/ActiveSupport/Concern.html&#34;&gt;Rails&amp;rsquo; Concerns&lt;/a&gt;. For our little demo app, it&amp;rsquo;d look something like this:&lt;/p&gt;
&lt;p&gt;Create a new &lt;code&gt;app/controllers/concerns/http_basic_auth.rb&lt;/code&gt; file and add the following code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Provides the `authenticate_http_basic` method which can be used to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# authenticate requests using credentials provided via Basic HTTP Auth.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;HttpBasicAuth&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;extend&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Concern&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  included &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;authenticate_http_basic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; authenticate_with_http_basic { |un, pw| do_authentication(un, pw) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      request_http_basic_authentication
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;do_authentication&lt;/span&gt;(username, password)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# TODO: Implement custom credentials validation logic.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      username == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;username&amp;#34;&lt;/span&gt; &amp;amp;&amp;amp; password == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;password&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And in the controller that we want to restrict access to, all we need is this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PagesController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;HttpBasicAuth&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  before_action &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:authenticate_http_basic&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;home&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If the entire app needs Basic HTTP Auth then adding this to the base &lt;code&gt;ApplicationController&lt;/code&gt; might make more sense. If only some actions need to be behind the authentication, then the &lt;a href=&#34;https://guides.rubyonrails.org/action_controller_overview.html#filters&#34;&gt;&lt;code&gt;:only&lt;/code&gt; and &lt;code&gt;:except&lt;/code&gt; modifiers&lt;/a&gt; can be used for finer grained control.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s all for now! It may be surprising to some but sometimes even Basic HTTP Auth is necessary. For those cases, Rails has our backs.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Using Devise for Authentication in Rails Without Database Stored Accounts</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/01/using-devise-for-authentication-without-database-stored-accounts/"/>
      <id>https://www.endpointdev.com/blog/2023/01/using-devise-for-authentication-without-database-stored-accounts/</id>
      <published>2023-01-19T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/01/using-devise-for-authentication-without-database-stored-accounts/sunset-plane.webp&#34; alt=&#34;A plane flies low over a lake, which reflecs the orange sky of a sunset. The lake is backed by tall mountains which are given depth by the end of the day&amp;rsquo;s haze.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2021 --&gt;
&lt;p&gt;We can pretty much say that thanks to the venerable &lt;a href=&#34;https://github.com/heartcombo/devise&#34;&gt;Devise&lt;/a&gt; &lt;a href=&#34;https://rubygems.org/&#34;&gt;gem&lt;/a&gt;, the authentication problem has been solved in &lt;a href=&#34;https://rubyonrails.org/&#34;&gt;Ruby on Rails&lt;/a&gt;. There are some instances however, when the requirements veer a little further away from convention and some customization needs to happen.&lt;/p&gt;
&lt;p&gt;Such was the case of a recent project where we had to implement authentication and session management on a small web application that would serve as an &lt;a href=&#34;https://microservices.io/patterns/apigateway.html&#34;&gt;API gateway&lt;/a&gt; into other system components. The interesting part was that there was no database to store accounts, and credentials would have to be validated against an external web service.&lt;/p&gt;
&lt;p&gt;Luckily for us, the Devise gem is customizable enough to be able to fulfill this requirement via custom authentication strategies. With custom authentication strategies, one can implement completely bespoke authentication logic while still enjoying a lot of the features that Devise offers out of the box.&lt;/p&gt;
&lt;p&gt;In this article, we&amp;rsquo;re going to walk through doing just that. Let&amp;rsquo;s get started.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To learn more about this capability of Devise, and how it relates to the underlying &lt;a href=&#34;https://github.com/wardencommunity/warden&#34;&gt;Warden&lt;/a&gt; concepts, here are some interesting sources:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://4trabes.com/2012/10/31/remote-authentication-with-devise/&#34;&gt;Remote authentication with devise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://insights.kyan.com/devise-authentication-strategies-a1a6b4e2b891&#34;&gt;Devise authentication strategies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://blog.plataformatec.com.br/2019/01/custom-authentication-methods-with-devise/&#34;&gt;Custom authentication methods with Devise&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/wardencommunity/warden/wiki/Strategies&#34;&gt;Warden strategies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;setting-up-a-new-rails-project&#34;&gt;Setting up a new Rails project&lt;/h3&gt;
&lt;p&gt;If you want to work along with me, and you like &lt;a href=&#34;https://www.docker.com/&#34;&gt;Docker&lt;/a&gt; and &lt;a href=&#34;https://code.visualstudio.com/&#34;&gt;VS Code&lt;/a&gt;, take a look at &lt;a href=&#34;/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/&#34;&gt;this blog post&lt;/a&gt; to learn how to set up a containerized development environment for Ruby on Rails with minimal effort.&lt;/p&gt;
&lt;p&gt;If not, you can follow &lt;a href=&#34;https://www.ruby-lang.org/en/documentation/installation/&#34;&gt;the official docs&lt;/a&gt; for installing Ruby.&lt;/p&gt;
&lt;p&gt;Either way, once you have your environment ready, we can go ahead and start setting up our new Rails project.&lt;/p&gt;
&lt;p&gt;First, install the rails gem:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ gem install rails&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, make sure you are located in the directory where you want to create the new project and do:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ rails new . --minimal&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;--minimal&lt;/code&gt; is an option to &lt;code&gt;rails new&lt;/code&gt; added in version 6.1 that disables a lot of default features and gems. You can learn more about it &lt;a href=&#34;https://github.com/rails/rails/pull/39282&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;installing-devise&#34;&gt;Installing Devise&lt;/h3&gt;
&lt;p&gt;Now, we need to add the Devise gem by adding this to your &lt;code&gt;Gemfile&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;devise&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And then running the following to install it in the project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails generate devise:install&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let&amp;rsquo;s also create a &amp;ldquo;home&amp;rdquo; page which we will point the app root route to. Run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails generate controller Pages home --no-helper&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That will create a new &lt;code&gt;PagesController&lt;/code&gt; with a &lt;code&gt;home&lt;/code&gt; action and its corresponding view.&lt;/p&gt;
&lt;p&gt;Next, in the &lt;code&gt;config/routes.rb&lt;/code&gt; file, replace the &lt;code&gt;get &#39;pages/home&#39;&lt;/code&gt; line created by the previous command with &lt;code&gt;root &amp;quot;pages#home&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Also, like I said at the beginning, we won&amp;rsquo;t use a database for storing accounts or validating credentials. So, we can comment out this line on the Devise initializer at &lt;code&gt;config/initializers/devise.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Devise&lt;/span&gt;.setup &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |config|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ==&amp;gt; ORM configuration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;## require &amp;#39;devise/orm/active_record&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we have a strong enough foundation to get started. Run &lt;code&gt;bin/rails server&lt;/code&gt; and navigate to the URL that the Puma development server produced and you should see this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/using-devise-for-authentication-without-database-stored-accounts/homepage.png&#34; alt=&#34;A browser viewing localhost:3001. On the page, a header reads &amp;ldquo;Pages#home&amp;rdquo;, with a sentence below reading &amp;ldquo;Find me in app/views/pages/home.html.erb&amp;rdquo;.&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;defining-the-user-model&#34;&gt;Defining the User model&lt;/h3&gt;
&lt;p&gt;For Devise to work, it needs to be associated with a model class from our app that represents user accounts. Normally, this would be some &lt;code&gt;ActiveRecord::Base&lt;/code&gt; derived class, like most conventional implementations. For us, though, this will not work because, like I said at the beginning, we don&amp;rsquo;t have a database. Neither accounts nor credentials will be stored locally, but rather in an external web service.&lt;/p&gt;
&lt;p&gt;So what we do instead is define a class that looks &lt;em&gt;enough&lt;/em&gt; like an ActiveRercord such that Devise can work with it. Thanks to Rails&amp;rsquo; modularity, this is easy, as we only need to include a few modules into said class. Here&amp;rsquo;s what our &lt;code&gt;User&lt;/code&gt; model class ends up looking like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/models/user.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;User&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveModel&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;API&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveModel&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Validations&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;extend&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveModel&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Callbacks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;extend&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Devise&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Models&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  define_model_callbacks &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:validation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;attr_accessor&lt;/span&gt; &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:password&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As you can see, all we have done is &lt;code&gt;include&lt;/code&gt; and &lt;code&gt;extend&lt;/code&gt; various modules as well as define the validation callbacks. This will augment the class with the APIs necessary for Devise to be able to interact with it properly. We also define email and password fields which will be our credentials.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can read more about &lt;code&gt;ActiveModel::Callbacks&lt;/code&gt; and &lt;code&gt;define_model_callbacks&lt;/code&gt; &lt;a href=&#34;https://api.rubyonrails.org/classes/ActiveModel/Callbacks.html&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Now that this class is available, in order to tell Devise that it needs to work with it, we add the following line to the &lt;code&gt;config/routes.rb&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; Rails.application.routes.draw do
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   # ...
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+  devise_for :users
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; end
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;implementing-the-custom-authentication-strategy&#34;&gt;Implementing the custom authentication strategy&lt;/h3&gt;
&lt;p&gt;Now that Devise is hooked up, let&amp;rsquo;s finally implement the custom authentication strategy. For this we will take three steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write a component that implements the authentication logic.&lt;/li&gt;
&lt;li&gt;Write a component that implements the serialization logic.&lt;/li&gt;
&lt;li&gt;Register the new strategy in the initializer and model class.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let&amp;rsquo;s get started with step 1. Here&amp;rsquo;s what the class looks like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/custom_auth/devise/strategies/custom_authenticatable.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Based on https://gist.github.com/madtrick/3917079&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Devise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Strategies&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# The class needs to inherit from Devise::Strategies::Authenticatable which&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# implements most of the underlying logic for auth strategies in Devise.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# You can read the code here:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# https://github.com/heartcombo/devise/blob/main/lib/devise/strategies/authenticatable.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;CustomAuthenticatable&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Authenticatable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# This is the method called by Warden to authenticate a user.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# More info in https://github.com/wardencommunity/warden/wiki/Strategies#authenticate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;authenticate!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; credentials_valid?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#888&#34;&gt;# Signals Warden that the authentication was successful.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#888&#34;&gt;# Expects an instance of the model class that Devise is configured to work with.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#888&#34;&gt;# This should be the user account that matches the provided credentials.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          success!(validated_user)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#888&#34;&gt;# Signals Warden that the authentication failed.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#038&#34;&gt;fail&lt;/span&gt;!
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# Returns a boolean indicating whether the provided credentials are valid.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# This is where you can implement any bespoke logic to do so: Read a file,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# call a service, validate a one-time-use token, etc.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# authentication_hash is provided by the base class and includes all the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# fields included in the login form.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;credentials_valid?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        authentication_hash[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;] == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;test@email.com&amp;#34;&lt;/span&gt; &amp;amp;&amp;amp; authentication_hash[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:password&lt;/span&gt;] == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;password&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;validated_user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# mapping.to is a reference to the model class that Devise is configured&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# to use that represents user accounts. In this case, it&amp;#39;s the User class.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        mapping.to.new(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;email&lt;/span&gt;: authentication_hash[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;password&lt;/span&gt;: authentication_hash[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:password&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Feel free to read through that code and the comments for some explanations on what each piece does. There are a few key points to go over.&lt;/p&gt;
&lt;p&gt;First of all, the class needs to inherit from &lt;code&gt;Devise::Strategies::Authenticatable&lt;/code&gt;. This is the base class that Devise provides for us to create custom authentication strategies. The most important method that we need to define is &lt;code&gt;authenticate!&lt;/code&gt;. This method is invoked by the powers that be (i.e. Warden) to run the actual authentication logic. After calling it, Warden expects it to invoke either &lt;code&gt;success!&lt;/code&gt; or &lt;code&gt;fail!&lt;/code&gt;, depending on the authentication results, and the rest of the system will act accordingly. &lt;code&gt;success!&lt;/code&gt; expects an instance of User being passed as an argument. This User instance represents the account that matches the given credentials.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;authenticate!&lt;/code&gt; method is where Devise and Warden give you the ability to fully customize how to authenticate the given credentials. I&amp;rsquo;ve chosen to implement a simple check against hardcoded values for our example here, but this can be anything. In my real world use case, I had to make a call to an external web service, but you could also check a file, validate a token with some algorithm, anything that&amp;rsquo;s possible with Ruby and Rails really.&lt;/p&gt;
&lt;p&gt;The next step is to implement the logic to serialize and deserialize the instance of User that represents the logged-in account to and from the Rails session store. This is encapsulated in a very simple module that looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/custom_auth/devise/models/custom_authenticatable.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Based on https://gist.github.com/madtrick/3916999&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Devise&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Models&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;CustomAuthenticatable&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080&#34;&gt;extend&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Concern&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ClassMethods&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Recreates a resource from session data.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;#&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# It takes as many params as elements in the array returned in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# serialize_into_session.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;serialize_from_session&lt;/span&gt;(email)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#080&#34;&gt;new&lt;/span&gt;(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;email&lt;/span&gt;:)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Returns an array with the data from the user that needs to be&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# serialized into the session.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;serialize_into_session&lt;/span&gt;(user)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          [user.email]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And this one is very self-explanatory. There&amp;rsquo;s a &lt;code&gt;serialize_into_session&lt;/code&gt; method that receives the User instance for the logged-in account and returns an array of the individual fields of it that we want Devise to include in the session data that persists across requests. And there&amp;rsquo;s also a &lt;code&gt;serialize_from_session&lt;/code&gt; method that receives the fields from the serialized array as individual parameters and has to return a new User instance.&lt;/p&gt;
&lt;p&gt;And finally, we need to register the new strategy and put it to work. We do that by adding these lines in the Devise initializer at &lt;code&gt;config/initializers/devise.rb&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Devise&lt;/span&gt;.setup &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |config|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  config.warden &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |manager|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    manager.strategies.add(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:custom&lt;/span&gt;, &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Devise&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Strategies&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;CustomAuthenticatable&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    manager.default_strategies(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;scope&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:user&lt;/span&gt;).unshift(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:custom&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Devise&lt;/span&gt;.add_module &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:custom_authenticatable&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;controller&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:sessions&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;route&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:session&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;strategy&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:custom&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, we&amp;rsquo;ve registered our new custom authentication strategy with Warden and added it as a default. We&amp;rsquo;ve also declared a new Devise &amp;ldquo;module&amp;rdquo; (via the &lt;a href=&#34;https://www.rubydoc.info/github/plataformatec/devise/Devise.add_module&#34;&gt;&lt;code&gt;Devise.add_module&lt;/code&gt;&lt;/a&gt; method) that will use that strategy (as given by the &lt;code&gt;strategy:&lt;/code&gt; parameter) and act as part of the sessions-related functionality (as given by the &lt;code&gt;controller:&lt;/code&gt; and &lt;code&gt;route:&lt;/code&gt; parameters). This means that, by adding this module to our User model class, Devise will render the routes necessary for session handling. Namely, &lt;code&gt;sign_in&lt;/code&gt; and &lt;code&gt;sign_out&lt;/code&gt;. Like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; $ bin/rails routes
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              Prefix Verb   URI Pattern               Controller#Action
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    new_user_session GET    /users/sign_in(.:format)  devise/sessions#new
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        user_session POST   /users/sign_in(.:format)  devise/sessions#create
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;destroy_user_session DELETE /users/sign_out(.:format) devise/sessions#destroy&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, all that&amp;rsquo;s left is adding the module to the &lt;code&gt;User&lt;/code&gt; class at &lt;code&gt;app/models/user.rb&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;User&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  devise &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:custom_authenticatable&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;authentication_keys&lt;/span&gt;: [&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:password&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note how we specify the &lt;code&gt;authentication_keys&lt;/code&gt; to be &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt;, the same fields that &lt;code&gt;Devise::Strategies::CustomAuthenticatable&lt;/code&gt; uses to run the validation logic, and are defined as attributes in &lt;code&gt;User&lt;/code&gt;. These can be anything really. The nice thing about using &lt;code&gt;email&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; in particular is that Devise&amp;rsquo;s default views and controllers already work with these fields, so no additional changes and configurations need to be done if we follow that convention.&lt;/p&gt;
&lt;p&gt;With all this, we&amp;rsquo;re basically plugging some of our code right into the middle of Devise&amp;rsquo;s logic for authentication and leveraging the rest of the functionality provided by it, like automatic views and controllers and such. As we&amp;rsquo;ll see next.&lt;/p&gt;
&lt;h3 id=&#34;implementing-session-management&#34;&gt;Implementing session management&lt;/h3&gt;
&lt;p&gt;Now that we&amp;rsquo;ve laid out all the piping that we need, the rest of the work to add support for sessions to our little test app is actually just business as usual with Devise.&lt;/p&gt;
&lt;p&gt;By adding the &lt;code&gt;custom_authenticatable&lt;/code&gt; Devise module to the User class, and the &lt;code&gt;devise_for :users&lt;/code&gt; line on the &lt;code&gt;config/routes.rb&lt;/code&gt; file, the session management actions are already in. So all we have to do is add a few links to actually use them. Adding the following snippet on &lt;code&gt;app/views/layouts/application.html.erb&lt;/code&gt; inside the &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; element will suffice:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;% if user_signed_in? %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;span&lt;/span&gt;&amp;gt;Hello &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= current_user.email %&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;span&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= button_to &amp;#34;Log out&amp;#34;, destroy_user_session_path, method: :delete %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;% else %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= link_to &amp;#34;Log in&amp;#34;, new_user_session_path %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;% end %&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;notice&amp;#34;&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= notice %&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;p&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;class&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;alert&amp;#34;&lt;/span&gt;&amp;gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&lt;/span&gt;%= alert %&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;p&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This should be very familiar if you&amp;rsquo;ve worked with Devise before. If not, there are more details &lt;a href=&#34;https://github.com/heartcombo/devise/wiki/How-To:-Add-sign_in,-sign_out,-and-sign_up-links-to-your-layout-template&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All this does is show a link to the log in page when there&amp;rsquo;s no user logged in; and a hello message along with a log out button when there is.&lt;/p&gt;
&lt;p&gt;Run the app with &lt;code&gt;bin/rails server&lt;/code&gt;, click the &amp;ldquo;Log in&amp;rdquo; link, and you&amp;rsquo;ll see Devise&amp;rsquo;s default log in page. Type in the hardcoded credentials from &lt;code&gt;Devise::Strategies::CustomAuthenticatable&lt;/code&gt; and submit the form to see something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/using-devise-for-authentication-without-database-stored-accounts/logged-in.png&#34; alt=&#34;Alt text&#34;&gt;&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve just been redirected back to the root and are greeted and shown a result message courtesy of Devise.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s it! We&amp;rsquo;ve developed a pretty simple custom authentication strategy for Devise that&amp;rsquo;s very straightforward. It does not use a database at all!&lt;/p&gt;
&lt;p&gt;The important thing though, is that we can use this pattern to implement any number of more complex procedures. And we&amp;rsquo;ve done so in a way that does not lock us out of leveraging the rest of Devise&amp;rsquo;s out-of-the-box capabilities like automatic controllers and views.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Developing Rails Apps in a Dev Container with VS Code</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/"/>
      <id>https://www.endpointdev.com/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/</id>
      <published>2023-01-13T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/icy-cave.webp&#34; alt=&#34;Icicles hang down from the opening of a cave, amid water falling into a pool lined with thick ice. Light from the cave&amp;rsquo;s opening illuminates the bottom corner of the image, opposite the icicles.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen --&gt;
&lt;p&gt;One of the great gifts from the advent of &lt;a href=&#34;https://www.docker.com/&#34;&gt;Docker&lt;/a&gt; and &lt;a href=&#34;https://www.docker.com/resources/what-container/&#34;&gt;containers&lt;/a&gt; is the ability to get a good development environment up and running very quickly. Regardless of programming language or tech stack, there is probably an image in &lt;a href=&#34;https://hub.docker.com/&#34;&gt;DockerHub&lt;/a&gt; or elsewhere that you can use to set up a container for development, either verbatim or as a basis for more complex setups.&lt;/p&gt;
&lt;p&gt;Moreover, even if your development environment is complex, once you have containerized it, it&amp;rsquo;s easy to replicate for new team members.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://code.visualstudio.com/&#34;&gt;VS Code&lt;/a&gt;, one of the most popular editors/IDEs today, with help from the &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers&#34;&gt;Dev Containers&lt;/a&gt; extension, makes the task of setting up a container for software development easier than ever.&lt;/p&gt;
&lt;p&gt;To demonstrate that, we&amp;rsquo;re going to walk through setting up such an environment for developing Ruby on Rails applications.&lt;/p&gt;
&lt;h3 id=&#34;setting-up-a-ruby-dev-container&#34;&gt;Setting up a Ruby Dev Container&lt;/h3&gt;
&lt;p&gt;As I alluded to before, all we need is Docker, VS Code, and the extension. Once you have those &lt;a href=&#34;https://www.docker.com/get-started/&#34;&gt;installed&lt;/a&gt;, we can easily create a new Docker container ready for Ruby on Rails development and have VS Code connect to it, resulting in a fully featured development environment.&lt;/p&gt;
&lt;h3 id=&#34;creating-the-configuration-file&#34;&gt;Creating the configuration file&lt;/h3&gt;
&lt;p&gt;To get started, create a new directory and open it in VS Code with something like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ mkdir ruby-dev-container
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ &lt;span style=&#34;color:#038&#34;&gt;cd&lt;/span&gt; ruby-dev-container
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ code .&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, in VS Code, bring up the &lt;a href=&#34;https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette&#34;&gt;Command Palette&lt;/a&gt; with &lt;code&gt;Ctrl + Shift + P&lt;/code&gt;. In it, run the &amp;ldquo;Dev Containers: Add Dev Container Configuration Files&amp;hellip;&amp;rdquo; command. In the resulting menu, select &amp;ldquo;Show All Definitions&amp;hellip;&amp;rdquo; option and look for Ruby.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/add-dev-container-config-files-1.png&#34; alt=&#34;A pop-up reading &amp;ldquo;Add Dev Container Configuration Files. The cursor is in a search box reading &amp;ldquo;Select a container configuration template&amp;rdquo;. Selected is the option reading &amp;ldquo;Show All Definitions&amp;hellip;&amp;rdquo;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/add-dev-container-config-files-2.png&#34; alt=&#34;The Ruby option, selected in the Add Dev Container Configuration Files pop-up&#34;&gt;&lt;/p&gt;
&lt;p&gt;From here on, the menu will ask you to select a version of Ruby and whether you want to include any additional features in the resulting development container. At the time of writing, 3.1 was the latest Ruby that appeared, so I selected that. Also, we don&amp;rsquo;t need any additional features, so I selected none.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There are many options here for different languages and tech stacks. For example, there&amp;rsquo;s Ruby, which we selected, but there are also ones that come out of the box with &lt;a href=&#34;https://sinatrarb.com/&#34;&gt;Sinatra&lt;/a&gt;, Rails, and even &lt;a href=&#34;https://www.postgresql.org/&#34;&gt;Postgres&lt;/a&gt;. Feel free to peruse! You can learn more about Dev Containers in &lt;a href=&#34;https://containers.dev/&#34;&gt;the official site&lt;/a&gt; and on &lt;a href=&#34;https://github.com/devcontainers&#34;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Anyway, after going through that menu and clicking the OK button, the extension will have produced a &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt; file with these contents:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Ruby&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#888&#34;&gt;// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;	&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;image&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;mcr.microsoft.com/devcontainers/ruby:0-3.1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;customizing-the-dev-container&#34;&gt;Customizing the Dev Container&lt;/h3&gt;
&lt;p&gt;As you can see, this is a JSON file that specifies what the dev container will look like. The most important part is the &lt;code&gt;image&lt;/code&gt; field which defines the image that the container will be running. In this case, it&amp;rsquo;s the &amp;ldquo;Ruby 3.1&amp;rdquo; image provided by the &lt;a href=&#34;https://mcr.microsoft.com/&#34;&gt;Microsoft Artifact Registry&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I like to add a few lines to this file to further configure the container. It ends up looking like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Ruby&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#888&#34;&gt;// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;	&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;image&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;mcr.microsoft.com/devcontainers/ruby:0-3.1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#888&#34;&gt;// Use &amp;#39;forwardPorts&amp;#39; to make a list of ports inside the container available locally.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;	&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;forwardPorts&amp;#34;&lt;/span&gt;: [&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3000&lt;/span&gt;],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#888&#34;&gt;// Configure tool-specific properties.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;	&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;customizations&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#888&#34;&gt;// Configure properties specific to VS Code.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;		&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;vscode&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#888&#34;&gt;// Add the IDs of extensions you want installed when the container is created.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;			&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;extensions&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;rebornix.Ruby&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;&amp;quot;forwardPorts&amp;quot;: [3000]&lt;/code&gt; makes the container&amp;rsquo;s port 3000 reachable from the local host machine&amp;rsquo;s browser. That means that whenever we do &lt;code&gt;bin/rails server&lt;/code&gt; from inside the container, we will be able to navigate to the app from our browser.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;customizations&lt;/code&gt; section installs the &lt;a href=&#34;https://marketplace.visualstudio.com/items?itemName=rebornix.Ruby&#34;&gt;Ruby VS Code extension&lt;/a&gt; in the container. This is surely optional but makes the development experience a little bit more fun. You can add any extensions you&amp;rsquo;d like here.&lt;/p&gt;
&lt;h3 id=&#34;running-the-container&#34;&gt;Running the Container&lt;/h3&gt;
&lt;p&gt;Now, to actually run and connect VS Code to a new container using the image and configs specified in the &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt; file; bring up the Command Palette again with &lt;code&gt;Ctrl + Shift + P&lt;/code&gt; and run the &amp;ldquo;Dev Containers: Reopen in Container&amp;rdquo; command.&lt;/p&gt;
&lt;p&gt;With that, VS Code will invoke Docker to download the image and create a new container based on it. It will also run and configure the container based on what&amp;rsquo;s specified in &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt;, and finally, connect to it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When that&amp;rsquo;s done, you should be able to see the container running with &lt;code&gt;docker ps&lt;/code&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;That will take a while, but once it&amp;rsquo;s done, you&amp;rsquo;ll be able to open VS Code&amp;rsquo;s integrated terminal (with &lt;code&gt;Ctrl + `&lt;/code&gt;), which will bring up a bash session in the container. From here, we can finally run all our usual Ruby and Rails commands to set up our project.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Feel free to explore the container&amp;rsquo;s environment. &lt;code&gt;ruby -v&lt;/code&gt; for example will show that Ruby is ready to go in there.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;creating-the-new-project&#34;&gt;Creating the new project&lt;/h3&gt;
&lt;p&gt;First, install the rails gem:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ gem install rails&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, create the new project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ rails new . --minimal&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And finally, run the app:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ bin/rails server&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Open a browser and navigate to &lt;a href=&#34;http://127.0.0.1:3000/&#34;&gt;http://127.0.0.1:3000/&lt;/a&gt; to see the classic Rails hello world screen:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/developing-rails-apps-in-a-dev-container-with-vs-code/hello-rails.png&#34; alt=&#34;Hello Rails. A browser navigated to http://127.0.0.1:3000/, with the webpage displaying the Rails logo, and underneath reading &amp;ldquo;Rails version: 7.0.4&amp;rdquo;; &amp;ldquo;Ruby version: ruby 3.1.3p185 (2022-11-24 revision 1a6b16756e)[x86_64-linux]&#34;&gt;&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s all! I use Ruby on Rails on a daily basis, so that&amp;rsquo;s what I&amp;rsquo;ve chosen to demonstrate here. However, there are many more options of programming languages and technologies when it comes to Dev Containers. All of them share very similar setup processes.&lt;/p&gt;
&lt;p&gt;And even if there isn&amp;rsquo;t an image optimized for development readily available in the Microsoft Artifact Registry, you can always author your own custom Dockerfile and use that for whatever use case you may have.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Updating Ruby on Rails</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2023/01/updating-rails/"/>
      <id>https://www.endpointdev.com/blog/2023/01/updating-rails/</id>
      <published>2023-01-12T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2023/01/updating-rails/blue-sky.webp&#34; alt=&#34;A blue sky with sparse clouds, framed by the tops of two buildings viewed from below.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2023 --&gt;
&lt;p&gt;Updating your app to the latest versions of the framework it was built on, and dependencies it uses, is an important part of the development process. It may seem like a waste to invest time and money into it, but it can bring as much value as a new feature.&lt;/p&gt;
&lt;p&gt;One good thing about using a framework like Ruby on Rails is that security features are baked in. This saves development time as the developer doesn&amp;rsquo;t have to re-create the wheel for logins, permissions, authentication, etc. There are many users of the framework who work together to can catch and patch vulnerabilities. Unfortunately, this means if your app hasn&amp;rsquo;t been updated its weaknesses become more obvious. A black hat attacker has easy access to a &lt;a href=&#34;https://www.cvedetails.com/vulnerability-list/vendor_id-12043/product_id-22569/Rubyonrails-Rails.html&#34;&gt;list of past Rails vulnerabilities&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Have you ever been to a &lt;a href=&#34;https://www.spacejam.com/&#34;&gt;website that hasn&amp;rsquo;t been updated for a while&lt;/a&gt; and found that everything moves slower than you&amp;rsquo;re used to? As technology improves and functions are optimized application processing time can be reduced. Most releases come with a performance update that can help your application keep up with the best of them.&lt;/p&gt;
&lt;p&gt;The gems your application uses also come out with updates to add new features and functionality. Usually these releases are made to work with the latest Rails versions. If you want to utilize them you&amp;rsquo;ll have to meet the minimum Rails version requirements.&lt;/p&gt;
&lt;h3 id=&#34;taking-it-step-by-step&#34;&gt;Taking it step by step&lt;/h3&gt;
&lt;p&gt;When updating your Rails app, you can&amp;rsquo;t just jump to the latest version. Updating to the next major release one at a time is recommended. This allows you to fix bugs as you go rather than trying to figure out what change broke what code.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://rubygems.org/gems/rails/versions&#34;&gt;The full list of Rails releases can be found here.&lt;/a&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Luckily you don&amp;rsquo;t have to go through each one of these releases. Releases including &lt;code&gt;rc&lt;/code&gt; or &lt;code&gt;beta&lt;/code&gt; (&lt;code&gt;4.2.10.rc1&lt;/code&gt;, &lt;code&gt;6.0.0.beta1&lt;/code&gt;) can be skipped. Small releases can also be skipped, such as going from &lt;code&gt;6.0.0&lt;/code&gt; to &lt;code&gt;6.0.1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you wanted to update from &lt;code&gt;4.0.0&lt;/code&gt; to &lt;code&gt;7.0.4&lt;/code&gt;, I&amp;rsquo;d recommend this route:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;4.0.0&lt;/code&gt; → &lt;code&gt;4.1.0&lt;/code&gt; → &lt;code&gt;4.2.0&lt;/code&gt; → &lt;code&gt;5.0.0&lt;/code&gt; → &lt;code&gt;5.1.0&lt;/code&gt; → &lt;code&gt;5.2.0&lt;/code&gt; → &lt;code&gt;6.0.0&lt;/code&gt; → &lt;code&gt;6.1.0&lt;/code&gt; → &lt;code&gt;7.0.0&lt;/code&gt; → &lt;code&gt;7.0.4&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Any of these updates can introduce bugs, but the biggest pain points will be the major version changes: 4 to 5, 5 to 6, etc.&lt;/p&gt;
&lt;h3 id=&#34;fixing-bundle-errors&#34;&gt;Fixing bundle errors&lt;/h3&gt;
&lt;p&gt;After changing the Rails and Ruby versions in the &lt;code&gt;Gemfile&lt;/code&gt;, run &lt;code&gt;bundle update rails&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source &amp;#39;http://rubygems.org&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ruby &amp;#39;2.7.0&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem &amp;#39;rails&amp;#39;, &amp;#39;~&amp;gt; 7.0.0&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some gems won&amp;rsquo;t be happy with the Rails update and will also need to be updated. Once this is done run &lt;code&gt;bundle update rails&lt;/code&gt; again. If there are more errors, fix them. If it&amp;rsquo;s successful &lt;code&gt;bundle install&lt;/code&gt; can then be run. This will ensure all the gems are properly installed.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/updating-rails/bundle-error.png&#34; alt=&#34;A bundling error saying &amp;ldquo;Bundler could not find compatible versions for gem &amp;ldquo;nokigiri in Gemfile&amp;rdquo;, followed by a list of several dependencies with incorrect resolutions.&#34;&gt;&lt;/p&gt;
&lt;p&gt;Also make sure to start up your server and verify there&amp;rsquo;s no problems there.&lt;/p&gt;
&lt;p&gt;No more errors — that means we&amp;rsquo;re done, right?&lt;/p&gt;
&lt;p&gt;Well&amp;hellip;&lt;/p&gt;
&lt;h3 id=&#34;manual-testing&#34;&gt;Manual testing&lt;/h3&gt;
&lt;p&gt;Even if your app builds correctly that doesn&amp;rsquo;t mean different parts of your app didn&amp;rsquo;t explode. Functions that worked in a previous version might have been deprecated in the newest version.&lt;/p&gt;
&lt;p&gt;For example, I came across this error in a recent update:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2023/01/updating-rails/find-all-by.png&#34; alt=&#34;Find all error. A red header reads &amp;ldquo;NoMethodError in HomeController#index&amp;rdquo;. The body reads &amp;ldquo;undefined method find_all_by_is_featured for #, followed by a code block showing the location of the error in the extracted source - in this case, line 5, reading @featured_cds = CompactDisc.find_all_by_is_featured(true).&#34;&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;find_all_by_X&lt;/code&gt; function was no longer working. Thankfully Stack Overflow is &lt;a href=&#34;https://stackoverflow.com/questions/59445783/undefined-method-find-all-by-x&#34;&gt;there to help&lt;/a&gt; us understand what changes were made and how to fix them. This function was deprecated in Rails 4, so &lt;code&gt;S.find_all_by_X_ID(x_ids)&lt;/code&gt; functions need to change to &lt;code&gt;S.where(x_id: x_ids)&lt;/code&gt;. Knowing this I was able to search the codebase for other &lt;code&gt;find_all_by_&lt;/code&gt; functions and change all of them.&lt;/p&gt;
&lt;p&gt;For large Rails updates (&lt;code&gt;5.2.0&lt;/code&gt; → &lt;code&gt;6.0.0&lt;/code&gt;), a full test of the app will need to be done. All functionality and pages should be verified and fixes applied.&lt;/p&gt;
&lt;p&gt;For smaller Rails updates (&lt;code&gt;4.0.0&lt;/code&gt; → &lt;code&gt;4.1.0&lt;/code&gt;), you can get away with a smaller test suite. You can stick to testing the big, commonly used features of the application. Of course, if you want to be thorough, a full test can also be done.&lt;/p&gt;
&lt;p&gt;Sometimes a gem will no longer be supported by the developer and no update has been made to allow it to work with the latest version of Rails. We have two options on how to proceed:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Find a replacement gem to accomplish the same functionality. Every reference to the original gem will need to be updated and fully tested.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Fork off of the gem and add the fixes yourself. Developers tend to stick with the gems they know across multiple applications. Owning the gem can be good as this updated version can be used on multiple apps by you and others. You&amp;rsquo;ll need to update the Gemfile to point at this new repository.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gem &amp;#39;dead_gem&amp;#39;, git: &amp;#39;https://github.com/githubUser/dead_gem&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;updating-remaining-gems&#34;&gt;Updating remaining gems&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re in the home stretch now. Rails is now on the most recent version and everything in the app is working correctly.&lt;/p&gt;
&lt;p&gt;The last recommended task is to update all gems to their latest versions. This will open up the gems&amp;rsquo; latest features and make future Ruby on Rails updates easier.&lt;/p&gt;
&lt;p&gt;Gems should be updated individually by running the command &lt;code&gt;bundle update #GemName&lt;/code&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Rails developer job opening</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/11/rails-developer-job/"/>
      <id>https://www.endpointdev.com/blog/2021/11/rails-developer-job/</id>
      <published>2021-11-03T00:00:00+00:00</published>
      <author>
        <name>Jon Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/02/job-rails-vuejs-developer/25677176162_c54b9effec_o-crop.jpg&#34; alt=&#34;Two software developers at their computers&#34; /&gt;&lt;br /&gt;
&lt;a href=&#34;https://www.flickr.com/photos/wocintechchat/25677176162/&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;https://www.wocintechchat.com/&#34;&gt;WOCinTech Chat&lt;/a&gt;, &lt;a href=&#34;https://creativecommons.org/licenses/by/2.0/&#34;&gt;CC BY 2.0&lt;/a&gt;, cropped&lt;/p&gt;
&lt;p&gt;We are seeking a full-time &lt;strong&gt;Ruby on Rails software developer based in the United States&lt;/strong&gt; to work with us on our clients’ applications.&lt;/p&gt;
&lt;p&gt;End Point Dev is an Internet technology consulting company based in New York City, with 50 employees serving many clients ranging from small family businesses to large corporations. The company is going strong after 26 years in business!&lt;/p&gt;
&lt;p&gt;Even before the pandemic most of us worked remotely from home offices. We collaborate using SSH, Git, project tracking tools, Zulip chat, video conferencing, and of course email and phones.&lt;/p&gt;
&lt;h3 id=&#34;what-you-will-be-doing&#34;&gt;What you will be doing:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Develop new web applications and support existing ones for our clients.&lt;/li&gt;
&lt;li&gt;Work together with End Point Dev co-workers and our clients’ in-house staff.&lt;/li&gt;
&lt;li&gt;Use your desktop operating system of choice: Linux, macOS, or Windows.&lt;/li&gt;
&lt;li&gt;Enhance open source software and contribute back as opportunity arises.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;youll-need-professional-development-experience-with&#34;&gt;You&amp;rsquo;ll need professional development experience with:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;4+ years of development with Ruby on Rails&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;JavaScript frameworks and libraries such as Vue.js, React, Angular&lt;/li&gt;
&lt;li&gt;Databases such as PostgreSQL, Redis, Solr, Elasticsearch, etc.&lt;/li&gt;
&lt;li&gt;Security consciousness, such as under HIPAA for safe medical &amp;amp; PII data handling&lt;/li&gt;
&lt;li&gt;Git version control&lt;/li&gt;
&lt;li&gt;Automated testing&lt;/li&gt;
&lt;li&gt;HTTP, REST APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;you-have-these-important-work-traits&#34;&gt;You have these important work traits:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Strong verbal and written communication skills&lt;/li&gt;
&lt;li&gt;An eye for detail&lt;/li&gt;
&lt;li&gt;Tenacity in solving problems and focusing on customer needs&lt;/li&gt;
&lt;li&gt;A feeling of ownership of your projects&lt;/li&gt;
&lt;li&gt;Work both independently and as part of a team&lt;/li&gt;
&lt;li&gt;A good remote work environment&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;For some of our clients you will need to submit to and pass a criminal background check.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;what-work-here-offers&#34;&gt;What work here offers:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Collaboration with knowledgeable, friendly, helpful, and diligent co-workers around the world&lt;/li&gt;
&lt;li&gt;Freedom from being tied to an office location&lt;/li&gt;
&lt;li&gt;Flexible, sane work hours&lt;/li&gt;
&lt;li&gt;Paid holidays and vacation&lt;/li&gt;
&lt;li&gt;Health insurance subsidy and 401(k) retirement savings plan&lt;/li&gt;
&lt;li&gt;Annual bonus opportunity&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;get-in-touch-with-us&#34;&gt;Get in touch with us:&lt;/h3&gt;
&lt;p&gt;&lt;del&gt;Please email us an introduction to &lt;a href=&#34;mailto:jobs@endpointdev.com&#34;&gt;jobs@endpointdev.com&lt;/a&gt; to apply.&lt;/del&gt;
&lt;strong&gt;(This job has been filled.)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Include your location, a resume/​CV, your Git repository or LinkedIn URLs, and whatever else may help us get to know you.&lt;/p&gt;
&lt;p&gt;We look forward to hearing from you! Direct work seekers only, please—​this role is not for agencies or subcontractors.&lt;/p&gt;
&lt;p&gt;We are an equal opportunity employer and value diversity at our company. We do not discriminate on the basis of sex/​gender, race, religion, color, national origin, sexual orientation, age, marital status, veteran status, or disability status.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Integrating the Estes Freight Shipping SOAP API as a Spree Shipping Calculator</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/09/estes-shipping-spree/"/>
      <id>https://www.endpointdev.com/blog/2021/09/estes-shipping-spree/</id>
      <published>2021-09-28T00:00:00+00:00</published>
      <author>
        <name>Patrick Lewis</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/09/estes-shipping-spree/20190617-084228-small.jpg&#34; alt=&#34;Cargo ship on sea with dark clouds&#34;&gt;&lt;/p&gt;
&lt;!-- photo by Jon Jensen --&gt;
&lt;p&gt;One of our clients with a Spree-based e-commerce site was interested in providing automated shipping quotes to their customers using their freight carrier &lt;a href=&#34;https://www.estes-express.com/&#34;&gt;Estes&lt;/a&gt;. After doing some research I found that Estes provided a variety of SOAP APIs and determined a method for extending Spree with custom shipping rate calculators. This presented an interesting challenge to me on several levels: most of my previous API integration experience was with &lt;a href=&#34;https://en.wikipedia.org/wiki/Representational_state_transfer&#34;&gt;REST&lt;/a&gt;, not &lt;a href=&#34;https://en.wikipedia.org/wiki/SOAP&#34;&gt;SOAP&lt;/a&gt; APIs, and I had not previously worked on custom shipping calculators for Spree. Fortunately, the Estes SOAP API documentation and some code examples of other Spree shipping calculators were all I needed to create a successful integration of the freight shipping API for this client.&lt;/p&gt;
&lt;h3 id=&#34;estes-api-documentation&#34;&gt;Estes API Documentation&lt;/h3&gt;
&lt;p&gt;The Estes &lt;a href=&#34;https://www.estes-express.com/resources/digital-services/api/rate-quote-web-service-v4-0&#34;&gt;Rate Quote Web Service&lt;/a&gt; API is the one that I relied on for being able to generate shipping quotes based on a combination of source address, destination address, and package weight. I found the developer documentation to be thorough and helpful, and was able to create working client code to send a request and receive a response relatively quickly. Many optional fields can be provided when making requests but I found that I only needed to use a small subset of these, as shown in the example code below.&lt;/p&gt;
&lt;p&gt;The one aspect of the API that tripped me up a bit was their use of &lt;code&gt;CN&lt;/code&gt; as the country code for Canada; Spree and most other codebases I have encountered use the international standard &lt;a href=&#34;https://www.iso.org/iso-3166-country-codes.html&#34;&gt;ISO 3166 country codes&lt;/a&gt; with &lt;code&gt;CA&lt;/code&gt; for Canada, so I had to add a small workaround for that in my client code when requesting shipping quotes to Canadian addresses. Another limitation I encountered is that the API expected to receive only 5-digit US and 6-character Canadian postal codes, so I had to do a bit of manipulation in my shipping calculator to account for that.&lt;/p&gt;
&lt;h3 id=&#34;ruby-soap-client&#34;&gt;Ruby SOAP Client&lt;/h3&gt;
&lt;p&gt;I researched Ruby SOAP clients and soon found &lt;a href=&#34;https://www.savonrb.com/&#34;&gt;Savon&lt;/a&gt;, the &amp;ldquo;Heavy metal SOAP client&amp;rdquo;, which proved to be very easy to integrate into my existing Rails/​Spree application. I added the savon gem to my project&amp;rsquo;s Gemfile and I was quickly able to instantiate a Savon client and configure it using the WSDL provided by Estes. After that, most of the integration work involved crafting a valid XML payload for my request and then parsing the response.&lt;/p&gt;
&lt;h3 id=&#34;spree-shipping-calculators&#34;&gt;Spree Shipping Calculators&lt;/h3&gt;
&lt;p&gt;The final piece of the puzzle was implementing the Savon client into a Spree shipping calculator class. This process allowed me to retrieve details about the current user&amp;rsquo;s order and then return the calculated shipping estimates within the context of the Spree checkout pages. Looking at existing shipping calculator code helped set me on the right path here; in the end, it was just a matter of defining a new class that inherited from the base &lt;code&gt;Spree::ShippingCalculator&lt;/code&gt; class and then defining the &lt;code&gt;#compute_package&lt;/code&gt; method expected by Spree for returning the shipping cost of a given package.&lt;/p&gt;
&lt;h3 id=&#34;code-example&#34;&gt;Code Example&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/models/spree/calculator/shipping/estes_calculator.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Spree::Calculator::Shipping&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# Custom freight shipping rate API integration&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;EstesCalculator&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Spree&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ShippingCalculator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ESTES_API_URL&lt;/span&gt; = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;https://www.estes-express.com/tools/rating/ratequote/v4.0/services/RateQuoteService?wsdl&amp;#39;&lt;/span&gt;.freeze
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;self&lt;/span&gt;.&lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;description&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Estes Freight&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;compute_package&lt;/span&gt;(package)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      country_code = package.order.ship_address.country.iso
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;unless&lt;/span&gt; country_code.in?([&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;US&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;CA&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;MX&amp;#39;&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      client = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Savon&lt;/span&gt;.client(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;filters&lt;/span&gt;: %i[user password account],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;log&lt;/span&gt;: &lt;span style=&#34;color:#080&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;log_level&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:debug&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;logger&lt;/span&gt;: &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Logger&lt;/span&gt;.new(&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.root.join(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;log&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;savon.log&amp;#39;&lt;/span&gt;)),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;pretty_print_xml&lt;/span&gt;: &lt;span style=&#34;color:#080&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;wsdl&lt;/span&gt;: &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ESTES_API_URL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      country_code = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;CN&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; country_code == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;CA&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#888&#34;&gt;# Estes uses CN for Canada&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      postal_code = package.order.ship_address.zipcode
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      postal_code =
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; country_code == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;CN&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          postal_code.delete(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39; &amp;#39;&lt;/span&gt;).first(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;6&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          postal_code.first(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      xml = &lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;~XML
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;soapenv:Envelope&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#369&#34;&gt;xmlns:soapenv=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;http://schemas.xmlsoap.org/soap/envelope/&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#369&#34;&gt;xmlns:rat=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;http://ws.estesexpress.com/ratequote&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#369&#34;&gt;xmlns:rat1=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;http://ws.estesexpress.com/schema/2019/01/ratequote&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;soapenv:Header&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat:auth&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat:user&amp;gt;&lt;/span&gt;#{ENV[&amp;#39;ESTES_USER&amp;#39;]}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat:user&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat:password&amp;gt;&lt;/span&gt;#{ENV[&amp;#39;ESTES_PASSWORD&amp;#39;]}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat:password&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat:auth&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/soapenv:Header&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;soapenv:Body&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:rateRequest&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:requestID&amp;gt;&lt;/span&gt;#{package.order.number}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:requestID&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:account&amp;gt;&lt;/span&gt;#{ENV[&amp;#39;ESTES_ACCOUNT&amp;#39;]}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:account&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:originPoint&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:countryCode&amp;gt;&lt;/span&gt;US&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:countryCode&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:postalCode&amp;gt;&lt;/span&gt;10001&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:postalCode&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:originPoint&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:destinationPoint&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:countryCode&amp;gt;&lt;/span&gt;#{country_code}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:countryCode&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:postalCode&amp;gt;&lt;/span&gt;#{postal_code}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:postalCode&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:destinationPoint&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:payor&amp;gt;&lt;/span&gt;S&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:payor&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:terms&amp;gt;&lt;/span&gt;C&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:terms&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:baseCommodities&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:commodity&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:class&amp;gt;&lt;/span&gt;50&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:class&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;rat1:weight&amp;gt;&lt;/span&gt;#{package_weight(package)}&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:weight&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:commodity&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:baseCommodities&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/rat1:rateRequest&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/soapenv:Body&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/soapenv:Envelope&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      XML&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      response = client.call(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:get_quote&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;xml&lt;/span&gt;: xml)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      quotes = response.body.dig(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:rate_quote&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:quote_info&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:quote&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; quotes.is_a?(&lt;span style=&#34;color:#038&#34;&gt;Array&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        quotes.first.dig(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:pricing&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:total_price&lt;/span&gt;).to_f
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;elsif&lt;/span&gt; quotes.is_a?(&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Hash&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        quotes.dig(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:pricing&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:total_price&lt;/span&gt;).to_f
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;rescue&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Savon&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Error&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# Record shipping rate as 0 if an API error is caught, 0 amount will indicate need to show user an error message on the shipping rate page&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;package_weight&lt;/span&gt;(package)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      weight = package.contents.sum { |content| content.line_item.weight }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; weight &amp;lt; &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#888&#34;&gt;# enforce minimum package weight&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        weight.round
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I was pleased that I was able to quickly build a custom shipping calculator in Spree that used the Estes Rate Quote API to provide accurate shipping estimates for large freight packages. The high quality documentation of the Estes API and the Savon SOAP Client gem made for a pleasant development experience, and the client was happy to gain the new functionality for their Spree store.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Enumerated Types in Rails and PostgreSQL</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/04/enumerated-types-rails-postgresql/"/>
      <id>https://www.endpointdev.com/blog/2021/04/enumerated-types-rails-postgresql/</id>
      <published>2021-04-29T00:00:00+00:00</published>
      <author>
        <name>Patrick Lewis</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/04/enumerated-types-rails-postgresql/banner.jpg&#34; alt=&#34;enumeration&#34;&gt;
&lt;a href=&#34;https://flic.kr/p/euTck&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;https://flickr.com/people/generated/&#34;&gt;Jared Tarbell&lt;/a&gt;, used under &lt;a href=&#34;https://creativecommons.org/licenses/by/2.0/&#34;&gt;CC BY 2.0&lt;/a&gt;, cropped from original.&lt;/p&gt;
&lt;p&gt;Enumerated types are a useful programming tool when dealing with variables that have a predefined, limited set of potential values. An example of an enumerated type from &lt;a href=&#34;https://en.wikipedia.org/wiki/Enumerated_type&#34;&gt;Wikipedia&lt;/a&gt; is “the four suits in a deck of playing cards may be four enumerators named Club, Diamond, Heart, and Spade, belonging to an enumerated type named suit”.&lt;/p&gt;
&lt;p&gt;I use enumerated types in my Rails applications most often for model attributes like “status” or “category”. Rails’ implementation of enumerated types in &lt;a href=&#34;https://api.rubyonrails.org/classes/ActiveRecord/Enum.html&#34;&gt;ActiveRecord::Enum&lt;/a&gt; provides a way to define sets of enumerated types and automatically makes some convenient methods available on models for working with enumerated attributes. The simple syntax does belie some potential pitfalls when it comes to longer-term maintenance of applications, however, and as I’ll describe later in this post, I would caution against using this basic 1-line syntax in most cases:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;status&lt;/span&gt;: [&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:active&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:archived&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The Rails implementation of enumerated types maps values to integers in database rows by default. This can be surprising the first time it is encountered, as a Rails developer looking to store status values like “active” or “archived” would typically create a string-based column. Instead, Rails looks for an numeric type column and stores the index of the selected enumerated value (0 for active, 1 for archived, etc.).&lt;/p&gt;
&lt;p&gt;This exposes one of the first potential drawbacks of this minimalist enumerated type implementation: the stored integer values can be difficult to interpret outside the context of the Rails application. Although querying records in a Rails console will map the integer values back to their enumerated equivalents, other database clients are simply going to return the mapped integer values instead, leaving it up to the developer to look up what those 0 or 1 values are supposed to represent.&lt;/p&gt;
&lt;p&gt;A larger problem that arises from defining an enum as an array of values is that the values are tied to the order of elements in an array. This means any change to the order or length of the array can have unwanted consequences on the mapped values.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# don&amp;#39;t do this; the index of active is changed from 0 to 1, archived from 1 to 2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;status&lt;/span&gt;: [&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:abandoned&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:active&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:archived&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At a minimum, I would recommend using this hash-based syntax for defining enumerated types with explicit integer mapping:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;status&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;active&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;archived&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This provides the benefit of documenting which integers are mapped to which enumerated values, and also provides more flexibility for future adjustments. For example, a new status value can now be added to the enumerated type without disrupting any of the existing records:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;status&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;abandoned&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;active&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;archived&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For Rails applications with PostgreSQL databases, it’s possible to go one step further and get most of the best of both worlds: the efficiency of using predefined enumerated types while still maintaining the ability to store meaningful string values at the database level. This is made possible by combining Rails enums with &lt;a href=&#34;https://www.postgresql.org/docs/current/datatype-enum.html&#34;&gt;PostgreSQL Enumerated Types&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This technique requires using a migration to first define a new enumerated type in the database, and then creating a column in the model’s table to use that PostgreSQL type:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AddEnumeratedStatusToDevices&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveRecord&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Migration&lt;/span&gt;[&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt;.&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;up&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    execute &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;lt;&amp;lt;-SQL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;CREATE&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;TYPE&lt;/span&gt; device_status &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;AS&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ENUM&lt;/span&gt; (&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;abandoned&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;active&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;archived&amp;#39;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    add_column &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:devices&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:status&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:device_status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    add_index &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:devices&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;down&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    remove_index &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:devices&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    remove_column &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:devices&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:status&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    execute &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;lt;&amp;lt;-SQL
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&lt;/span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;DROP&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;TYPE&lt;/span&gt; device_status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;SQL&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The corresponding model code to use this new enumerated type looks similar to before, but now the values are mapped to strings:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Device&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;status&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;abandoned&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;abandoned&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;active&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;active&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;archived&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;archived&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This combination of Rails and PostgreSQL enumerated types has become my preferred approach in most situations. One limitation to be aware of with this approach is that PostgreSQL enumerated types can be extended with &lt;a href=&#34;https://www.postgresql.org/docs/current/sql-altertype.html&#34;&gt;ALTER TYPE&lt;/a&gt;, but existing values cannot be removed. There is a small bit of additional development overhead introduced with the need to manage the enumerated type at both the Rails and the PostgreSQL level, but I like having the option of querying records by the string values of attributes, and the use of a PostgreSQL enumerated type provides for more efficient database storage than simply using a string type column.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Job opening: Ruby on Rails &amp; Vue.js developer</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2021/02/job-rails-vuejs-developer/"/>
      <id>https://www.endpointdev.com/blog/2021/02/job-rails-vuejs-developer/</id>
      <published>2021-02-24T00:00:00+00:00</published>
      <author>
        <name>Jon Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2021/02/job-rails-vuejs-developer/25677176162_c54b9effec_o-crop.jpg&#34; alt=&#34;Two software developers at their computers&#34; /&gt;&lt;br&gt;
&lt;a href=&#34;https://www.flickr.com/photos/wocintechchat/25677176162/&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;https://www.wocintechchat.com/&#34;&gt;WOCinTech Chat&lt;/a&gt;, &lt;a href=&#34;https://creativecommons.org/licenses/by/2.0/&#34;&gt;CC BY 2.0&lt;/a&gt;, cropped&lt;/p&gt;
&lt;p&gt;We are seeking a full-time software engineer specializing in Ruby on Rails and Vue.js to work with us on our clients’ applications.&lt;/p&gt;
&lt;p&gt;End Point is an Internet technology consulting company based in New York City, founded 25 years ago! We have over 50 employees serving many clients ranging from small family businesses to large corporations.&lt;/p&gt;
&lt;p&gt;Even before the pandemic most of us worked remotely from home offices. We collaborate using SSH, Git, project tracking tools, Zulip chat, video conferencing, and of course email and phones.&lt;/p&gt;
&lt;h3 id=&#34;what-you-will-be-doing&#34;&gt;What you will be doing:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Develop new web applications and support existing ones for our clients&lt;/li&gt;
&lt;li&gt;Consult with clients on software features and plans&lt;/li&gt;
&lt;li&gt;Work together with End Point co-workers and our clients’ in-house staff&lt;/li&gt;
&lt;li&gt;Use open source tools and contribute back as opportunity arises&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;what-you-bring&#34;&gt;What you bring:&lt;/h3&gt;
&lt;p&gt;Professional experience developing and supporting web applications in these technical areas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;3+ years of development with Ruby on Rails and front-end JavaScript&lt;/li&gt;
&lt;li&gt;Frameworks and libraries such as Vue.js, React, Angular&lt;/li&gt;
&lt;li&gt;Databases such as PostgreSQL, MySQL, Redis, Solr, Elasticsearch, etc.&lt;/li&gt;
&lt;li&gt;Security consciousness&lt;/li&gt;
&lt;li&gt;Git version control&lt;/li&gt;
&lt;li&gt;Automated testing&lt;/li&gt;
&lt;li&gt;HTTP, REST APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These work traits are just as important:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Strong verbal and written communication skills&lt;/li&gt;
&lt;li&gt;An eye for detail&lt;/li&gt;
&lt;li&gt;Tenacity in solving problems and focusing on customer needs&lt;/li&gt;
&lt;li&gt;A feeling of ownership of your projects&lt;/li&gt;
&lt;li&gt;Work both independently and as part of a team&lt;/li&gt;
&lt;li&gt;Ability to pass a criminal background check when clients require&lt;/li&gt;
&lt;li&gt;A good remote work environment and self-discipline&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;what-work-here-offers&#34;&gt;What work here offers:&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Collaborate with knowledgeable, friendly, helpful, and diligent co-workers around the world&lt;/li&gt;
&lt;li&gt;Flexible, sane work hours&lt;/li&gt;
&lt;li&gt;Paid holidays and vacation&lt;/li&gt;
&lt;li&gt;Annual bonus opportunity&lt;/li&gt;
&lt;li&gt;Freedom from being tied to an office location&lt;/li&gt;
&lt;li&gt;Use your desktop OS of choice: Linux, macOS, Windows&lt;/li&gt;
&lt;li&gt;For U.S. employees: health insurance subsidy and 401(k) retirement savings plan&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;get-in-touch-with-us&#34;&gt;Get in touch with us:&lt;/h3&gt;
&lt;p&gt;&lt;del&gt;Please email us an introduction to jobs@endpointdev.com to apply.&lt;/del&gt;
&lt;strong&gt;(This job has been filled.)&lt;/strong&gt;
Include your location, a resume/​CV, your Git repository or LinkedIn URLs, and whatever else may help us get to know you.&lt;/p&gt;
&lt;p&gt;We look forward to hearing from you! Direct work seekers only, please—​this role is not for agencies or subcontractors.&lt;/p&gt;
&lt;p&gt;We are an equal opportunity employer and value diversity at our company. We do not discriminate on the basis of sex/​gender, race, religion, color, national origin, sexual orientation, age, marital status, veteran status, or disability status.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Availity: An API for Health Insurance</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2020/11/availity-api-health-insurance/"/>
      <id>https://www.endpointdev.com/blog/2020/11/availity-api-health-insurance/</id>
      <published>2020-11-16T00:00:00+00:00</published>
      <author>
        <name>Patrick Lewis</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2020/11/availity-api-health-insurance/banner.jpg&#34; alt=&#34;Stethoscope&#34;&gt;
&lt;a href=&#34;https://flic.kr/p/25e3v5L&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;https://www.flickr.com/people/143707811@N07/&#34;&gt;Sergio Santos&lt;/a&gt;, used under &lt;a href=&#34;https://creativecommons.org/licenses/by/2.0/&#34;&gt;CC BY 2.0&lt;/a&gt;, cropped from original.&lt;/p&gt;
&lt;p&gt;I have been working on a tele-therapy application for a client in the health care industry over the past few months and had the opportunity to do some interesting work in the area of health insurance coverages and claims.&lt;/p&gt;
&lt;p&gt;I was tasked with creating an integration of the &lt;a href=&#34;https://www.availity.com/&#34;&gt;Availity&lt;/a&gt; API for insurance coverages which provides the ability to make requests for details about a patient’s health insurance coverage and returns responses containing information like the patient’s primary care doctor, their copay amounts, and their deductibles.&lt;/p&gt;
&lt;p&gt;The ability to query this health insurance information from an API in an automated fashion helps streamline the process of billing clients by validating their health insurance details and also determining what a patient’s financial responsibility will be for their online therapy sessions. Availity provides information for over 11,000 insurance companies; a &lt;a href=&#34;https://apps.availity.com/public-web/payerlist-ui/payerlist-ui/#/&#34;&gt;full list of supported payers&lt;/a&gt; is available on their site via a web interface and a downloadable CSV file.&lt;/p&gt;
&lt;p&gt;Availity provides both REST and SOAP APIs in addition to a batch processing system that functions over SFTP. For the purposes of this article I will be focusing on the REST API which was the primary focus of my work for this project.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://developer.availity.com/partner/documentation&#34;&gt;developer documentation&lt;/a&gt; for the REST API was mostly self-serve; after signing up for an account on the Availity site I was able to rely on their publicly available documentation for all of the API details that I needed to start making requests.&lt;/p&gt;
&lt;p&gt;Unfortunately, the development process for integrating the SOAP API was not nearly as smooth; the &lt;a href=&#34;https://developer.availity.com/partner/&#34;&gt;SOAP APIs&lt;/a&gt; link on Availity’s main developer portal page currently comes up blank, and I had to register a separate account in order to create a support request to obtain documentation on the SOAP API. Even with that documentation in hand, I found it difficult to determine things like the correct WSDL to use, and the process for generating X12 strings was much more complicated than making a more traditional REST request with a parameter hash. The large majority of payers supported by Availity are covered by the REST API, but there are some that are only supported by SOAP API requests and necessitate this more difficult process.&lt;/p&gt;
&lt;p&gt;The application we were developing featured a Rails backend, so I used the Ruby &lt;a href=&#34;https://github.com/rest-client/rest-client&#34;&gt;rest-client&lt;/a&gt; gem when making requests to the Availity REST API.&lt;/p&gt;
&lt;p&gt;The request payload was surprisingly small. Most payers only require this combination of patient/provider details when making a request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Patient birth date&lt;/li&gt;
&lt;li&gt;Payer ID (Assigned by Availity to the health insurance company in the payer list linked above)&lt;/li&gt;
&lt;li&gt;Member ID (The patient’s membership ID with their insurer)&lt;/li&gt;
&lt;li&gt;Provider NPI&lt;/li&gt;
&lt;li&gt;Service type&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here is an excerpt of the API client code I created for making requests to the API:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Availity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Client&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080&#34;&gt;extend&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Limiter&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Mixin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Rate limit API requests to 100 per second&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# https://developer.availity.com/partner/node/503&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    limit_method &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:coverages&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;rate&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;100&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;interval&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;BASE_URL&lt;/span&gt; = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;https://api.availity.com/availity/v1&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;coverages&lt;/span&gt;(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;payer_id&lt;/span&gt;:, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;member_id&lt;/span&gt;:, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;patient_birth_date&lt;/span&gt;:, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;patient_first_name&lt;/span&gt;:, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;patient_last_name&lt;/span&gt;:)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      url = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;BASE_URL&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;/coverages&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      params = {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;payerID&lt;/span&gt;: payer_id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;memberId&lt;/span&gt;: member_id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;providerNpi&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;123456789&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;patientBirthDate&lt;/span&gt;: patient_birth_date,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;patientFirstName&lt;/span&gt;: patient_first_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;patientLastName&lt;/span&gt;: patient_last_name,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;serviceType&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;30&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      headers = {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;Authorization&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Bearer &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;token&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;params&lt;/span&gt;: params
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;RestClient&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Request&lt;/span&gt;.execute(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;method&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:get&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;url&lt;/span&gt;: url,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;headers&lt;/span&gt;: headers,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;log&lt;/span&gt;: &lt;span style=&#34;color:#33b&#34;&gt;@log&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The insurance coverage API then returns a link to a fairly long JSON response with a variety of information on the patient’s health insurance plan. I added an additional class to parse that response for specific details such as name of primary care doctor, total and remaining deductibles for the year, copay amounts, etc.&lt;/p&gt;
&lt;p&gt;In addition to this insurance coverage API, Availity provides various other &lt;a href=&#34;https://developer.availity.com/partner/node/503&#34;&gt;API end points&lt;/a&gt; related to health care claims and costs that could be equally valuable to the development of applications in the health care industry.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>COVID-19 Support for the Kansas Department of Health and Environment</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2020/09/covid-19-support-kansas-dept-health/"/>
      <id>https://www.endpointdev.com/blog/2020/09/covid-19-support-kansas-dept-health/</id>
      <published>2020-09-14T00:00:00+00:00</published>
      <author>
        <name>Steve Yoman</name>
      </author>
      <content type="html">
        &lt;h3 id=&#34;kansass-existing-epitrax-system&#34;&gt;Kansas’s existing EpiTrax system&lt;/h3&gt;
&lt;p&gt;End Point has worked on Kansas’s disease surveillance systems since 2011. In 2018 we migrated them from their legacy TriSano application to the open source EpiTrax surveillance system created by Utah’s Department of Health. The new EpiTrax system had been in full production for about eight months when COVID-19 cases started to grow in the United States.&lt;/p&gt;
&lt;h3 id=&#34;covid-19-help-needed&#34;&gt;COVID-19: Help needed&lt;/h3&gt;
&lt;p&gt;In March 2020, the Director of Surveillance Systems at the Kansas Department of Health and Environment (KDHE) asked us at End Point to create a web-based portal where labs, hospitals, and ad-hoc testing locations could enter COVID-19 test data. While systems existed for gathering data from labs and hospitals, they needed a way to quickly gather data from the many new and atypical sites collecting COVID-19 test information.&lt;/p&gt;
&lt;h3 id=&#34;our-approach&#34;&gt;Our approach&lt;/h3&gt;
&lt;p&gt;Since the portal was intended for people who were unfamiliar with the existing EpiTrax application, we were able to create a new design that was simple and direct, unconstrained by other applications. It required a self-registration function so users could access the system quickly and without administrative overhead, and users needed to understand how to use it without extensive training.&lt;/p&gt;
&lt;p&gt;We at End Point agreed to create the portal and dashboard for test results reporters immediately, while planning for later development of additional administrative functions. We used Ruby on Rails and Vue.js to build the portal due to their usefulness for rapid development. The Vue.js front-end JavaScript framework allowed us to quickly put together the portal UI and integrate it with the Rails back-end web services.&lt;/p&gt;
&lt;p&gt;Once approved, our team got to work setting up the environment, developing the portal application, and rigorously testing it.&lt;/p&gt;
&lt;p&gt;Here are some screenshots of the application:&lt;/p&gt;
&lt;h3 id=&#34;portal-home-page&#34;&gt;Portal home page&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2020/09/covid-19-support-kansas-dept-health/Home-Screen.jpg&#34; alt=&#34;Kansas Reportable Disease Portal home page&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;user-registration-page&#34;&gt;User registration page&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2020/09/covid-19-support-kansas-dept-health/newuser.png&#34; alt=&#34;Create New User Account page&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;dashboard&#34;&gt;Dashboard&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2020/09/covid-19-support-kansas-dept-health/dashboard.jpg&#34; alt=&#34;Dashboard for searching and browsing cases&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;reporters-entry-screens&#34;&gt;Reporters’ entry screens&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2020/09/covid-19-support-kansas-dept-health/Reporter-Data-Entry.jpg&#34; alt=&#34;Forms collecting information about reporter and patient&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2020/09/covid-19-support-kansas-dept-health/form-part-2.png&#34; alt=&#34;Forms collecting information about disease, speciment, and testing results&#34;&gt;&lt;/p&gt;
&lt;p&gt;The portal was launched on April 30.&lt;/p&gt;
&lt;h3 id=&#34;contact-tracers-support&#34;&gt;Contact tracers support&lt;/h3&gt;
&lt;p&gt;After the EpiTrax portal for COVID-19 was up and running, Kansas made urgent plans to hire 400 contact tracers. We updated the EpiTrax portal to accommodate this new type of user, showing contact tracers only the data they are authorized to view, while providing them the ability to make limited contact record updates and to create tasks. The project is ongoing, and eventually will be used for all diseases.&lt;/p&gt;
&lt;h3 id=&#34;epitrax-for-missouri&#34;&gt;EpiTrax for Missouri&lt;/h3&gt;
&lt;p&gt;Late this Spring, after learning of Kansas’s use of the EpiTrax platform and the powerful capabilities that it provides to the Kansas public health system, the State of Missouri reached out to us at End Point to request their own EpiTrax implementation. End Point and Missouri quickly came to agreement, and since July we have been working together build out a full production EpiTrax surveillance system for them.&lt;/p&gt;
&lt;h3 id=&#34;contact-us&#34;&gt;Contact us!&lt;/h3&gt;
&lt;p&gt;To discuss an EpiTrax project, &lt;a href=&#34;/contact/&#34;&gt;contact&lt;/a&gt; us. Our expert team is ready to help you meet your requirements.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Shopify Admin API: Importing Products in Bulk</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2020/05/shopify-product-creation/"/>
      <id>https://www.endpointdev.com/blog/2020/05/shopify-product-creation/</id>
      <published>2020-05-04T00:00:00+00:00</published>
      <author>
        <name>Patrick Lewis</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2020/05/shopify-product-creation/banner.jpg&#34; alt=&#34;Cash Register&#34;&gt;
&lt;a href=&#34;https://www.flickr.com/photos/mrvjtod/279845948/&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;https://www.flickr.com/photos/mrvjtod/&#34;&gt;Chris Young&lt;/a&gt;, used under &lt;a href=&#34;https://creativecommons.org/licenses/by-sa/2.0/&#34;&gt;CC BY-SA 2.0&lt;/a&gt;, cropped from original.&lt;/p&gt;
&lt;p&gt;I recently worked on an interesting project for a store owner who was facing a daunting task: he had an inventory of hundreds of thousands of &lt;a href=&#34;https://en.wikipedia.org/wiki/Magic%3A_The_Gathering&#34;&gt;Magic: The Gathering&lt;/a&gt; (MTG) cards that he wanted to sell online through his Shopify store. The logistics of tracking down artwork and current market pricing for each card made it impossible to do manually.&lt;/p&gt;
&lt;p&gt;My solution was to create a custom Rails application that retrieves inventory data from a combination of APIs and then automatically creates products for each card in Shopify. The resulting project turned what would have been a months- or years-long task into a bulk upload that only took a few hours to complete and allowed the store owner to immediately start selling his inventory online. The online store launch turned out to be even more important than initially expected due to current closures of physical stores.&lt;/p&gt;
&lt;h3 id=&#34;application-requirements&#34;&gt;Application Requirements&lt;/h3&gt;
&lt;p&gt;The main requirements for the Rails application were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retrieving product data for MTG cards by merging results from a combination of sources/APIs&lt;/li&gt;
&lt;li&gt;Mapping card attributes and metadata into the format expected by the Shopify Admin API for creating Product records&lt;/li&gt;
&lt;li&gt;Performing a bulk push of products to Shopify&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There were some additional considerations like staying within rate limits for both the inventory data and Shopify APIs, but I will address those further in a follow-up post.&lt;/p&gt;
&lt;h3 id=&#34;retrieving-card-artwork-and-pricing&#34;&gt;Retrieving Card Artwork and Pricing&lt;/h3&gt;
&lt;p&gt;I ended up using a combination of two APIs to retrieve MTG card data: &lt;a href=&#34;https://mtgjson.com/&#34;&gt;MTGJSON&lt;/a&gt; for card details like the name of the card and the set it belonged to, and &lt;a href=&#34;https://scryfall.com/&#34;&gt;Scryfall&lt;/a&gt; for retrieving card images and current market pricing. It was relatively easy to combine the two because MTGJSON provided Scryfall IDs for all of its records, allowing me to merge results from the two APIs together.&lt;/p&gt;
&lt;h3 id=&#34;working-with-the-shopify-admin-api-in-ruby&#34;&gt;Working With the Shopify Admin API in Ruby&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&#34;https://shopify.dev/docs/admin-api&#34;&gt;Shopify Admin API&lt;/a&gt; deals in terms of generic &lt;a href=&#34;https://shopify.dev/docs/admin-api/rest/reference/products/product&#34;&gt;Product&lt;/a&gt; records with predefined attributes like &lt;code&gt;title&lt;/code&gt; and &lt;code&gt;product_type&lt;/code&gt;. The official &lt;a href=&#34;https://github.com/Shopify/shopify_api&#34;&gt;shopify_api&lt;/a&gt; Ruby gem made it very easy to connect to my client’s Shopify store and create new products by creating &lt;code&gt;Shopify::Product&lt;/code&gt; objects with a hash of attributes like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  attrs = {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;images&lt;/span&gt;: [{ &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;src&lt;/span&gt;: scryfall_card.image_uris.large }],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;options&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Card Type&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Condition&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;product_type&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;MTG Singles&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;tags&lt;/span&gt;: card.setCode,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;title&lt;/span&gt;: card.name,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;variants&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;inventory_management&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;shopify&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;inventory_quantity&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;option1&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Foil&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;option2&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Like New&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;price&lt;/span&gt;: scryfall_card.prices.usd_foil
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Shopify&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Product&lt;/span&gt;.new(attrs).save&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The actual production code is a bit more complicated to account for outliers like cards with multiple “faces” and cards that come in both regular and foil variants, but the example above shows the basic shape of the attributes expected by the Shopify API.&lt;/p&gt;
&lt;h3 id=&#34;pushing-50000-products-to-shopify&#34;&gt;Pushing 50,000 Products to Shopify&lt;/h3&gt;
&lt;p&gt;After I completed testing with individual products and confirmed the ability to take a specific card and turn it into a Shopify product with artwork and pricing pre-populated it was time to perform the full upload of all 50,000+ cards in the MTGJSON database. I decided to use &lt;a href=&#34;https://sidekiq.org/&#34;&gt;Sidekiq&lt;/a&gt; and create jobs for each card upload so that I could rate limit the workers to stay within rate limits for both the Scryfall and Shopify APIs, and also have persistence that would allow me to pause/resume the queue or retry individual failed jobs.&lt;/p&gt;
&lt;p&gt;The Sidekiq approach to queueing up all of the card uploads worked great; I was able to use the Sidekiq dashboard to monitor the queue of 50,000 jobs as it worked its way through each card, and was able to see the Shopify products being created on the store in real time. Once the inventory was in place in Shopify the store owner was able to start updating his inventory levels and make cards available for sale via the Shopify Admin.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;A custom Ruby application using the Shopify API is a powerful solution for online storefronts that need to retrieve a large number of inventory data from external sources. I was pleased with how this project turned out; it was rewarding to create a custom application that leveraged several APIs and automated a task that would have been extremely repetitive, and probably impossibly time-consuming, to do manually. It was encouraging to do my first upload of a card and see it show up on the Shopify store with artwork, pricing, and card details pre-populated.&lt;/p&gt;
&lt;p&gt;The development model used for this project could be applied to stores in a wide variety of markets. This project used external APIs to retrieve product information but that data source could easily be replaced with a spreadsheet, CSV file, or some other export file containing bulk information on products to be sold.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Capturing Outgoing Email With Mock SMTP Servers</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2020/03/mock-smtp-servers/"/>
      <id>https://www.endpointdev.com/blog/2020/03/mock-smtp-servers/</id>
      <published>2020-03-13T00:00:00+00:00</published>
      <author>
        <name>Patrick Lewis</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2020/03/mock-smtp-servers/banner.jpg&#34; alt=&#34;Mailboxes&#34; /&gt; &lt;a href=&#34;http://web.archive.org/web/20190215161746/https://www.flickr.com/photos/seattleye/2891561034/&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;http://web.archive.org/web/20191122092817/https://www.flickr.com/photos/seattleye/&#34;&gt;Seattleye&lt;/a&gt;, used under &lt;a href=&#34;https://creativecommons.org/licenses/by/2.0/&#34;&gt;CC BY 2.0&lt;/a&gt;, cropped from original.&lt;/p&gt;
&lt;p&gt;Sending automated email to users is a common requirement of most web applications and can take the form of things like password reset emails or order confirmation invoices.&lt;/p&gt;
&lt;p&gt;It is important for developers working in development/staging environments to verify that an application is sending email correctly without &lt;em&gt;actually&lt;/em&gt; delivering messages to users’ inboxes. If you were testing a background task that searches an e-commerce site for abandoned shopping carts and emails users to remind them that they have not completed a checkout, you would not want to run that in development and end up repeatedly emailing live user email addresses.&lt;/p&gt;
&lt;p&gt;A mock SMTP server is useful for development and testing because it lets you configure the email settings of your development environment almost exactly the same as you would for outgoing SMTP email in your production site. The mock SMTP server will capture all of the outbound email and allow you to review it in a web interface instead of actually delivering it to users’ inboxes.&lt;/p&gt;
&lt;h3 id=&#34;mock-smtp-servers&#34;&gt;Mock SMTP Servers&lt;/h3&gt;
&lt;p&gt;There are a variety of standalone/free and hosted/commercial options for mock SMTP servers including:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mailhog/MailHog&#34;&gt;MailHog&lt;/a&gt; (free)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://mailslurper.com&#34;&gt;MailSlurper&lt;/a&gt; (free)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://mailcatcher.me&#34;&gt;MailCatcher&lt;/a&gt; (free)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://mailtrap.io&#34;&gt;Mailtrap&lt;/a&gt; (free for small solo projects/paid)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://mailosaur.com&#34;&gt;Mailosaur&lt;/a&gt; (paid)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The standalone/free options have been sufficient for the projects I have worked on. Some of the features offered by the hosted solutions like Mailtrap and Mailosaur may be appealing to larger development teams.&lt;/p&gt;
&lt;p&gt;MailHog is my go-to mock SMTP server because it has a nice web interface and is extremely easy to install and configure for typical use. The standalone solutions that I have tried all work similarly; they listen for SMTP email on one port, and provide a web interface on a separate port for reviewing captured email.&lt;/p&gt;
&lt;h3 id=&#34;configuring-a-rails-application-to-use-mailhog&#34;&gt;Configuring a Rails Application to use MailHog&lt;/h3&gt;
&lt;p&gt;Installation and use of MailHog is very simple: download and run the &lt;code&gt;mailhog&lt;/code&gt; executable to have it start listening for SMTP email on port 1025 and hosting a web interface on port 8025. Then edit the Rails development environment config file to send email using an SMTP server running on local port 1025:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/environments/development.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.application.configure &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# …&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  config.action_mailer.delivery_method = &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:smtp&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  config.action_mailer.perform_deliveries = &lt;span style=&#34;color:#080&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  config.action_mailer.smtp_settings = { &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;address&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;localhost&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;port&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1025&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# …&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;MailHog runs in the foreground and will display output as it receives outbound email. Opening a web browser to http://localhost:8025 shows MailHog’s web interface and allows you to view a list of the email that it has captured or click on an email to show its full details:&lt;/p&gt;
&lt;img src=&#34;/blog/2020/03/mock-smtp-servers/mailhog.png&#34; alt=&#34;Mailhog screenshot&#34; /&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;I have found MailHog to be helpful when developing a variety of different web applications, particularly ones that generate email for user notifications and confirmations. Thanks to its ease of installation and use, I would recommend it (or an equivalent mock SMTP solution) to any developer that needs to test outgoing email features in their web applications.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Rails Exception Notification and Logging</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2019/10/rails-exception-notification/"/>
      <id>https://www.endpointdev.com/blog/2019/10/rails-exception-notification/</id>
      <published>2019-10-31T00:00:00+00:00</published>
      <author>
        <name>Patrick Lewis</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2019/10/rails-exception-notification/banner.jpg&#34; alt=&#34;Railroad tracks&#34; /&gt; &lt;a href=&#34;https://flic.kr/p/dGLeY&#34;&gt;Photo&lt;/a&gt; by &lt;a href=&#34;https://flickr.com/photos/chefranden/&#34;&gt;Randen Pederson&lt;/a&gt;, used under &lt;a href=&#34;https://creativecommons.org/licenses/by-sa/2.0/&#34;&gt;CC BY-SA 2.0&lt;/a&gt;, cropped from original.&lt;/p&gt;
&lt;p&gt;Exception handling is an important component of any Rails application that is intended to be deployed in a production environment.&lt;/p&gt;
&lt;p&gt;If you have ever felt your heart sink because you received a screenshot of your Rails application with this dreaded “something went wrong” message from a customer or client, you know what I’m talking about:&lt;/p&gt;
&lt;img src=&#34;/blog/2019/10/rails-exception-notification/something-went-wrong.png&#34; alt=&#34;Rails application error&#34; /&gt;
&lt;p&gt;Though developers strive to deploy projects that are free of bugs and never throw an exception, the reality is that people using your Rails applications do sometimes encounter errors. When that happens it’s better to have your application automatically notify you of the event, rather than finding out about it for the first time when someone calls or emails you.&lt;/p&gt;
&lt;h3 id=&#34;exception-notification&#34;&gt;Exception Notification&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/smartinez87/exception_notification&#34;&gt;Exception Notification&lt;/a&gt; gem is a stalwart of the Ruby/​Rails ecosystem and the standard solution for configuring your Rails project to notify you when errors occur. I typically use the gem to deliver notifications to a development/​hosting team via email, but it has a variety of built-in notifiers that can output to just about any location you would want: Slack, IRC, Amazon SNS, etc., plus an option to write custom webhooks for other services that are not supported by default.&lt;/p&gt;
&lt;p&gt;Exception Notification is simple to install and configure, particularly for Rails applications where it can be &lt;a href=&#34;https://github.com/smartinez87/exception_notification#rails-1&#34;&gt;installed as a Rails engine&lt;/a&gt; and configured via an initializer. The default &lt;code&gt;config/initializers/exception_notification.rb&lt;/code&gt; initializer generated by the gem comes with an example of an email notifier:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Email notifier sends notifications by email.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;config.add_notifier &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;, {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;email_prefix&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;[ERROR] &amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;sender_address&lt;/span&gt;: &lt;span style=&#34;color:#2b2;background-color:#f0fff0&#34;&gt;%{&amp;#34;Notifier&amp;#34; &amp;lt;notifier@example.com&amp;gt;}&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;exception_recipients&lt;/span&gt;: &lt;span style=&#34;color:#2b2;background-color:#f0fff0&#34;&gt;%w{exceptions@example.com}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;along with examples of other configuration options for conditions to determine which errors should generate notifications and some examples of other notification providers.&lt;/p&gt;
&lt;p&gt;Starting with just an email notifier might be enough for your needs, but it’s worth reading through the Exception Notification documentation to find out what other capabilities it has, including options for rate-limiting to prevent your inbox from being inundated by recurring errors.&lt;/p&gt;
&lt;h3 id=&#34;exceptiontrack&#34;&gt;ExceptionTrack&lt;/h3&gt;
&lt;p&gt;A related Rails-specific gem called &lt;a href=&#34;https://github.com/rails-engine/exception-track&#34;&gt;ExceptionTrack&lt;/a&gt; adds another layer of exception tracking on top of Exception Notifier; it provides a mechanism for storing exceptions in your Rails application’s database, and provides a web interface (mounted as an engine inside your application) for reviewing them.&lt;/p&gt;
&lt;p&gt;I tend to use a combination of ExceptionTrack and Exception Notifier in my projects, using ExceptionTrack and Exception Notifier in production environments but not configuring any Exception Notifier deliveries in development. It is easy to configure the gems differently in development and production thanks to Rails’ separate environment config files.&lt;/p&gt;
&lt;p&gt;Generating database migration and config files with &lt;code&gt;rails g exception_track:install&lt;/code&gt; will create the following initializer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/initializers/exception-track.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# require &amp;#39;exception_notification/sidekiq&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExceptionTrack&lt;/span&gt;.configure &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# environments for store Exception log in to database.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# default: [:development, :production]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# self.environments = %i(development production)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ExceptionNotification.configure do |config|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#   config.ignored_exceptions += %w(ActionView::TemplateError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#                                   ActionController::InvalidAuthenticityToken&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#                                   ActionController::BadRequest&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#                                   ActionView::MissingTemplate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#                                   ActionController::UrlGenerationError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;#                                   ActionController::UnknownFormat)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;ExceptionTrack’s configuration options are limited to mostly specifying which environments you want it to store exceptions for; by default it is enabled for both development and production.&lt;/p&gt;
&lt;p&gt;After that it’s just a matter of running the database migration to create a table for storing ExceptionTrack’s data and then adding a new route to mount the web interface in your application, with something like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/routes.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  mount &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExceptionTrack&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Engine&lt;/span&gt; =&amp;gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;/exceptions&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.env.development?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you want to make ExceptionTrack’s web interface available in production, it is advisable to protect the route behind authentication so that only users with admin-level access can view it. The &lt;a href=&#34;https://github.com/mperham/sidekiq/wiki/Monitoring#authentication&#34;&gt;Sidekiq gem’s documentation&lt;/a&gt; has some examples for protecting access to routes based on different authentication systems that can be easily adapted to work with ExceptionTrack’s routing.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Access to easily-installable tools for tracking and logging exceptions is a nice benefit for Rails developers, and Exception Notification / ExceptionTrack make a powerful combination for anyone interested in a self-serve solution that includes logging and the ability to send notifications to a wide variety of services.&lt;/p&gt;
&lt;p&gt;Larger organizations and projects may benefit from a hosted service like &lt;a href=&#34;https://airbrake.io&#34;&gt;Airbrake&lt;/a&gt; for error monitoring, but the solutions provided here can work well for many Ruby/​Rails projects without the added expense of a managed third-party service.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Project On-ramping: Infrastructure and Codebase</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2019/10/codebase-on-ramp/"/>
      <id>https://www.endpointdev.com/blog/2019/10/codebase-on-ramp/</id>
      <published>2019-10-21T00:00:00+00:00</published>
      <author>
        <name>Steph Skardal</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2019/10/codebase-on-ramp/nasa.jpg&#34; alt=&#34;Aerial view of New York City from satellite&#34;&gt;
Photo by NASA via Unsplash&lt;/p&gt;
&lt;p&gt;Living in the consulting world, we often jump into codebases and environments to get quickly up to speed to provide estimates or start fixing bugs right away. Here are some important landmarks I visit along my way to get up to speed on a new project.&lt;/p&gt;
&lt;h3 id=&#34;dev-stack&#34;&gt;Dev stack&lt;/h3&gt;
&lt;p&gt;First up, I learn as much as I can about the dev stack a codebase runs on. It used to be the case that LAMP (Linux, Apache, MySQL, PHP) was a pretty common setup in our world, but the number of dev tools and variety in stacks has expanded greatly over time. Many of my End Point clients are now running nginx on Linux, with Ruby on Rails and MySQL or PostgreSQL, but as a company we have both breadth and depth in web stack technologies (hover over the Expertise dropdown above).&lt;/p&gt;
&lt;p&gt;Layered on top of the base infrastructure might be a JavaScript framework (e.g. &lt;a href=&#34;https://reactjs.org/&#34;&gt;React&lt;/a&gt;, &lt;a href=&#34;https://emberjs.com/&#34;&gt;Ember.js&lt;/a&gt;), and other abstractions to improve the dev process (e.g. &lt;a href=&#34;https://sass-lang.com/&#34;&gt;Sass&lt;/a&gt;, &lt;a href=&#34;https://nodejs.org/en/&#34;&gt;Node.js&lt;/a&gt;). And layered on top of that further is the possibility of other services running on the server locally (e.g. search using &lt;a href=&#34;https://www.elastic.co/&#34;&gt;Elasticsearch&lt;/a&gt; or &lt;a href=&#34;https://lucene.apache.org/solr/&#34;&gt;Solr&lt;/a&gt;) and 3rd-party tools running on a server elsewhere (e.g. content delivery networks, monitoring, &lt;a href=&#34;https://stripe.com/&#34;&gt;Stripe&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The web stack and 3rd-party tools can be so complex these days that there is a lot of ground to cover before jumping into the code.&lt;/p&gt;
&lt;h3 id=&#34;personnel-infrastructure&#34;&gt;Personnel infrastructure&lt;/h3&gt;
&lt;p&gt;Next up, I like to understand how our client works with the website and more about their limitations (time, technical, other). Especially in the consulting space where we are supporting existing developers, I think it’s important to exercise empathy over how they’ve gotten to the point they are at, and why they’ve reached out for additional development resources.&lt;/p&gt;
&lt;p&gt;Does the client want a solution that enables them to make their own changes moving forward, i.e., a content management solution? Do they want to hand the project off entirely due to time limitations? Is there a hybrid solution that will enable them to make changes and defer to us as a resource for bigger decisions? Is the existing development staff who we should be best supporting and advising on decisions?&lt;/p&gt;
&lt;h3 id=&#34;are-there-dev-instances&#34;&gt;Are there dev instances?&lt;/h3&gt;
&lt;p&gt;After we have a high-level understanding of the dev stack and personnel involved, the next bridge we cross is to determine dev instance architecture (and how to get access to a dev instance!). It goes without saying (though I’m saying it anyways) that it’s a much lower risk to experiment and play around on a dev instance rather than a production server.&lt;/p&gt;
&lt;p&gt;Here at End Point, we have our own in-house tool for spinning up dev instances (&lt;a href=&#34;https://www.devcamps.org/&#34;&gt;DevCamps&lt;/a&gt;), and we also work with clients who have their own setup for dev environments (e.g. &lt;a href=&#34;https://www.docker.com/&#34;&gt;Docker&lt;/a&gt;, &lt;a href=&#34;https://aws.amazon.com/&#34;&gt;AWS&lt;/a&gt;) that we can jump into.&lt;/p&gt;
&lt;h3 id=&#34;codebase-where-do-i-go&#34;&gt;Codebase: Where do I go?&lt;/h3&gt;
&lt;p&gt;I like to equate learning a new code base to moving to a new city. When you first arrive, you don’t know what you are looking at or where to go, but as you begin to make frequent trips to one location or another, you become familiar with those paths and you become most familiar with the paths that you take most often.&lt;/p&gt;
&lt;p&gt;One of my first stops is the database. I review the tables and understand how the application models interface with them, as well as how they reference each other. Another stop (in the Rails world) is to visit &lt;code&gt;config/routes.rb&lt;/code&gt;, which &lt;a href=&#34;https://guides.rubyonrails.org/routing.html&#34;&gt;maps URLs and dispatches them to a Rails controller&lt;/a&gt;. This immediately informs me of the complexity of the application by the number of different URL requests it handles.&lt;/p&gt;
&lt;p&gt;A third favorite stop upon gaining access to the codebase is the code history (hopefully there is one!), which can be telling about the organization as well as frequent code destinations visited.&lt;/p&gt;
&lt;p&gt;Another place to visit is the (again, in the Rails space) &lt;a href=&#34;https://bundler.io/gemfile.html&#34;&gt;Bundler Gemfile&lt;/a&gt;, which tells me how many (or how few) dependencies are included in a project.&lt;/p&gt;
&lt;p&gt;While I don’t think it’s important to get into the nitty gritty of Rails models, controllers, or views at a first pass, my observations of the database, routing, Git history, and Gemfile can paint a picture of the complexity and dependencies of one project. Outside of the Rails space, many modern web frameworks have comparable code paradigm that I think are similarly revealing.&lt;/p&gt;
&lt;h3 id=&#34;digging-deeper&#34;&gt;Digging deeper&lt;/h3&gt;
&lt;p&gt;From there, I look to client priorities for where to dig deeper into the code. I have found that in some cases in my consulting experience, the client priorities and convention use in codebase (e.g. with Ruby on Rails) overlaps nicely with my skill set and I’m able to jump right in to make changes, but other client work requires further research into third parties or historical context of the code and changes needed.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Campendium: A Responsive, Fancy Detail Page</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2019/09/campendium-detail-page/"/>
      <id>https://www.endpointdev.com/blog/2019/09/campendium-detail-page/</id>
      <published>2019-09-09T00:00:00+00:00</published>
      <author>
        <name>Steph Skardal</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2019/09/campendium-detail-page/banner.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;This week, I was very excited to deploy a project for &lt;a href=&#34;https://www.campendium.com/&#34;&gt;Campendium&lt;/a&gt;, one of our long-time clients. As noted in &lt;a href=&#34;/blog/2019/08/campendium-updates/&#34;&gt;my recent post on Campendium updates for the year&lt;/a&gt;, Campendium has thousands of listings of places to camp and provides a great infrastructure for the development of a rich community of travelers around North America.&lt;/p&gt;
&lt;p&gt;For the past few months, I’ve been working on a significant update to Campendium’s campground detail page, the page template where in-depth information is provided for each of Campendium’s locations. This is equivalent to a product detail page on an ecommerce site.&lt;/p&gt;
&lt;p&gt;The “guts” of the update included a new detail page design with expanded responsiveness, the introduction of 360° videos, and expanded user driven content in the form of Question &amp;amp; Answer (Q&amp;amp;A or QnA), reviews, notes, nightly rates, etc. Read on to find out more and see video examples of several of the features!&lt;/p&gt;
&lt;h3 id=&#34;user-interface--responsiveness-updates&#34;&gt;User Interface &amp;amp; Responsiveness Updates&lt;/h3&gt;
&lt;p&gt;One of the things the Campendium team and I are most proud of here is the responsiveness of the new design. In the case of traveling and camping, responsiveness is important since a large amount of traffic comes from mobile devices, relative to what you might see in other industries.&lt;/p&gt;
&lt;p&gt;User images are shown as “hero images” and the user interface updates depending on the browser device and width, as shown in the following videos for &lt;a href=&#34;https://www.campendium.com/gilbert-ray-campground&#34;&gt;Gilbert Ray Campground&lt;/a&gt; and &lt;a href=&#34;https://www.campendium.com/cayuga-lake-state-park&#34;&gt;Cayuga Lake State Park&lt;/a&gt;.&lt;/p&gt;
&lt;p style=&#34;text-align:center;font-weight:bold;&#34;&gt;&lt;iframe src=&#34;https://drive.google.com/file/d/1FFF8GcztnV-KGaJ4pjDYixCbkxw6kduu/preview&#34; width=&#34;770&#34; height=&#34;450&#34;&gt;&lt;/iframe&gt;A preview of responsive behavior on the campground detail page with user submitted photos.&lt;/p&gt;
&lt;p style=&#34;text-align:center;font-weight:bold;&#34;&gt;&lt;iframe src=&#34;https://drive.google.com/file/d/1caQZT9ogh_piuTX2UDIvWGEBZse0zwnx/preview&#34; width=&#34;770&#34; height=&#34;450&#34;&gt;&lt;/iframe&gt;A second preview of responsive behavior on the campground detail page with embedded maps.&lt;/p&gt;
&lt;p&gt;Another updated design usability tweak was a sticky navigation bar to navigation throughout the page, which can get especially long with user submitted content. See how the “Overview”, “Video”, etc. links become sticky as you scroll down on the page, and the current region coming into view of the page is underlined:&lt;/p&gt;
&lt;p style=&#34;text-align:center;font-weight:bold;&#34;&gt;&lt;iframe src=&#34;https://drive.google.com/file/d/1w_MjP_HUXntYXHTG6FeREtMWwhKv4KgG/preview&#34; width=&#34;770&#34; height=&#34;450&#34;&gt;&lt;/iframe&gt;Navigation becomes fixed to the top of the browser as a user scrolls through the content.&lt;/p&gt;
&lt;p&gt;Campendium uses Ruby on Rails as a backend, a bit of &lt;a href=&#34;https://getbootstrap.com/&#34;&gt;Bootstrap&lt;/a&gt;, and &lt;a href=&#34;https://sass-lang.com/&#34;&gt;Sass&lt;/a&gt; and best practice responsive design is used throughout this updated user interface.&lt;/p&gt;
&lt;h3 id=&#34;360-videos&#34;&gt;360° Videos&lt;/h3&gt;
&lt;p&gt;The Campendium team has been hard at work creating 360 degree videos of the various locations to provide significant value to their users. These videos are now embedded into the campground detail page. Video hosting is provided by YouTube and dropped into the Campendium campground HTML template.&lt;/p&gt;
&lt;p style=&#34;text-align:center;font-weight:bold;&#34;&gt;&lt;iframe src=&#34;https://drive.google.com/file/d/12XEu-95diRRMjlqb28jD0xdj9qIWXSb7/preview&#34; width=&#34;770&#34; height=&#34;450&#34;&gt;&lt;/iframe&gt;A video in a video—a 360 degree video example.&lt;/p&gt;
&lt;p&gt;I personally think these 360° videos are awesome, especially to those visual learners out there! You can learn so much from a video that can be hard to capture in user reviews.&lt;/p&gt;
&lt;h3 id=&#34;community-qa&#34;&gt;Community Q&amp;amp;A&lt;/h3&gt;
&lt;p&gt;Another new feature with this deploy is the introduction of community driven Question &amp;amp; Answer. Users can submit questions about a campground, and users who have contributed to this campground are invited to respond (or they can opt-out site-wide to responding to questions). In addition to Q&amp;amp;A itself, user content can be “upvoted” (marked as Helpful) or flagged for an improved user content browsing experience.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/09/campendium-detail-page/community.png&#34; alt=&#34;Campendium community in Question &amp;amp; Answer&#34;&gt;&lt;/p&gt;
&lt;p&gt;All Q&amp;amp;A functionality is driven by JavaScript coupled with the Ruby on Rails backend.&lt;/p&gt;
&lt;h3 id=&#34;expansion-of-user-contributed-content-and-features&#34;&gt;Expansion of User Contributed Content and Features&lt;/h3&gt;
&lt;p&gt;Finally, this update includes expansion of user contributed content in addition to Q&amp;amp;A. This includes the ability for users to contribute notes, cell signal reports, and nightly rates. Reviews can now be filtered by user profile information and sorted by date or specific ranking. Reviews can also be searched by keywords and those keywords highlighted in the results. The following video demonstrates sorting and searching of reviews with highlighted keyword match:&lt;/p&gt;
&lt;p style=&#34;text-align:center;font-weight:bold;&#34;&gt;&lt;iframe src=&#34;https://drive.google.com/file/d/1G2KGfxCkOmuJm-AJE8BPXUC-OWdw1iQS/preview&#34; width=&#34;770&#34; height=&#34;450&#34;&gt;&lt;/iframe&gt;Review searchability with highlighting and filterability.&lt;/p&gt;
&lt;p&gt;All search functionality driven by JavaScript on the frontend, and a Ruby on Rails backend coupled with &lt;a href=&#34;https://github.com/sunspot/sunspot&#34;&gt;Sunspot&lt;/a&gt; and &lt;a href=&#34;https://lucene.apache.org/solr/&#34;&gt;Solr&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;who-doesnt-love-stats&#34;&gt;Who Doesn’t Love Stats?&lt;/h3&gt;
&lt;p&gt;Just for the sake of sharing stats, from GitHub, this update included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;215 changed files&lt;/li&gt;
&lt;li&gt;6,284 additions&lt;/li&gt;
&lt;li&gt;3,924 deletions (removal / cleanup of unneeded JavaScript files)&lt;/li&gt;
&lt;/ul&gt;

      </content>
    </entry>
  
    <entry>
      <title>Campendium v2019: A Summary of Recent Updates</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2019/08/campendium-updates/"/>
      <id>https://www.endpointdev.com/blog/2019/08/campendium-updates/</id>
      <published>2019-08-05T00:00:00+00:00</published>
      <author>
        <name>Steph Skardal</name>
      </author>
      <content type="html">
        &lt;p&gt;This year has brought a handful of exciting changes for &lt;a href=&#34;https://www.campendium.com/&#34;&gt;Campendium&lt;/a&gt;, one of End Point’s long-time clients, by yours truly. Created by campers for campers, Campendium has thousands of listings of places to camp, from swanky RV parks to free remote destinations, vetted by a team of full-time travelers and reviewed by over 200,000 members. I thought I would take some time to summarize these recent updates.&lt;/p&gt;
&lt;h3 id=&#34;maps-and-clustering&#34;&gt;Maps and Clustering&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/08/campendium-updates/map-clusters.png&#34; alt=&#34;Campendium map clustering of campground locations&#34;&gt;&lt;/p&gt;
&lt;p&gt;Campendium uses &lt;a href=&#34;https://www.mapbox.com/&#34;&gt;Mapbox&lt;/a&gt; for map rendering to display campgrounds and locations throughout North America. One of the new features added this year was &lt;a href=&#34;https://docs.mapbox.com/mapbox-gl-js/example/cluster/&#34;&gt;clustering&lt;/a&gt; of campground locations, where campgrounds are grouped together and presented in a “cluster” with a size relative to how many campgrounds are in the cluster.&lt;/p&gt;
&lt;p&gt;If a user is searching for campgrounds in a broad location, they can see where campgrounds might be more densely grouped by location. Once a user zooms in zoom in a couple of clicks, the campgrounds are no longer clustered and individual campgrounds locations can be seen. While working on this update, we spent a good amount of our time tweaking and troubleshooting the optimal clustering behavior to provide the most benefit to those searching for a campground. &lt;a href=&#34;https://docs.mapbox.com/mapbox-gl-js/api/&#34;&gt;Mapbox GL JS&lt;/a&gt; works in parallel with &lt;a href=&#34;https://reactjs.org/&#34;&gt;ReactJS&lt;/a&gt;, and runs with a Ruby on Rails back-end.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/08/campendium-updates/map-non-clusters.jpg&#34; alt=&#34;Campendium map non-clustering of campground locations after zooming in&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;advanced-filtering&#34;&gt;Advanced Filtering&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/08/campendium-updates/map-filtering.jpg&#34; alt=&#34;Campendium advanced filtering&#34;&gt;&lt;/p&gt;
&lt;p&gt;Another exciting was the introduction of advanced filtering in the search interface, presented in combination with map display. Users can filter campgrounds by category (e.g. Public Land, RV Parking, Parking, Dump Station), filter by price (with a slider), hookups, campground policy (e.g. age or pet restrictions), discounts, recreation, and facilities. All of this search filtering is driven by &lt;a href=&#34;https://github.com/sunspot/sunspot&#34;&gt;Sunspot&lt;/a&gt;, a Ruby on Rails gem for working with the popular &lt;a href=&#34;https://lucene.apache.org/solr/&#34;&gt;Solr&lt;/a&gt; search engine. Results can be sorted by user provided reviews, price or distance from a specific GPS location. Here, much care was given to provide the best user interface for presenting this valuable functionality.&lt;/p&gt;
&lt;h3 id=&#34;supporters-only-features&#34;&gt;“Supporters Only” Features&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/08/campendium-updates/supporters.jpg&#34; alt=&#34;Campendium Supporters only, Subscriptions&#34;&gt;&lt;/p&gt;
&lt;p&gt;Another recent update to Campendium includes functionality to offer user subscriptions. Registered users can sign up to support Campendium on a monthly or annual basis, and subscriptions are set to auto-renew at the end of their subscription period. This paid support hides advertisements throughout the site (advertisements are controlled by a third party), and advanced filtering on cell reception. There are plans to expand supporter features in the future. Ruby on Rails combined with &lt;a href=&#34;https://stripe.com/docs/stripe-js/reference&#34;&gt;StripeJS&lt;/a&gt; is used to manage subscription payments, and Ruby on Rails also serves as a backend for &lt;a href=&#34;https://developer.apple.com/in-app-purchase/&#34;&gt;In-App Purchases&lt;/a&gt; of subscriptions from the App store.&lt;/p&gt;
&lt;h3 id=&#34;always-responsive-and-latency-aware&#34;&gt;Always Responsive and Latency-Aware&lt;/h3&gt;
&lt;p&gt;Because a large portion of the Campendium visitors are on the road, it’s important to have both a responsive design and to build for bandwidth limitations for users. Throughout the development of these new features, responsive and mobile friendly designs were implemented leveraging &lt;a href=&#34;https://sass-lang.com/&#34;&gt;Sass&lt;/a&gt;, sometimes requiring help from the rest of the knowledgeable &lt;a href=&#34;/team/&#34;&gt;End Point team&lt;/a&gt;!&lt;/p&gt;
&lt;p&gt;Many of the pages throughout the site are fully cached including the homepage, search result pages, and campground detail page, and cookies are used to indicate user status. In some cases, user submitted campground images are lazy-loaded to mitigate bandwidth limitations.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2019/08/campendium-updates/mobile.jpg&#34; alt=&#34;Mobile, responsive design for Campendium&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;whats-next&#34;&gt;What’s Next?&lt;/h3&gt;
&lt;p&gt;While I didn’t go into much technical depth on these updates, I am happy that the updates represent a broad spectrum of full-stack development skills featuring nginx, Ruby on Rails, 3rd party integration including StripeJS, MapboxGL and IAP (Apple), a JavaScript framework with ReactJS, and working with Ruby gems to leverage other tools, for example, Solr (Sunspot) and Sass.&lt;/p&gt;
&lt;p&gt;In the future, Campendium plans to continue using these tools to see a more interactive, social campground detail page, and has plans to expand outside of North America. You can visit Campendium &lt;a href=&#34;https://www.campendium.com/&#34;&gt;here&lt;/a&gt;, or find them on Instagram &lt;a href=&#34;https://www.instagram.com/campendium/?hl=en&#34;&gt;here&lt;/a&gt; to follow their exciting announcements!&lt;/p&gt;

      </content>
    </entry>
  
</feed>
