<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Renzo Diaz</title>
    <description>The latest articles on DEV Community by Renzo Diaz (@renzodiaz).</description>
    <link>https://dev.to/renzodiaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F284980%2F41b087c6-3041-4b49-a36a-24bc3db6aeae.jpg</url>
      <title>DEV Community: Renzo Diaz</title>
      <link>https://dev.to/renzodiaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/renzodiaz"/>
    <language>en</language>
    <item>
      <title>Build a Secure API with Rails 8 - Part-2: Authentication Foundations</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Wed, 13 May 2026 16:44:18 +0000</pubDate>
      <link>https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-2-authentication-foundations-2fo5</link>
      <guid>https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-2-authentication-foundations-2fo5</guid>
      <description>&lt;p&gt;Hey folks 👋&lt;/p&gt;

&lt;p&gt;Welcome back. In &lt;a href="https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-1-11lh"&gt;Part 1&lt;/a&gt; we walked through the 11 attack vectors that shape every decision in this series. If you skipped it, please go read it first, because everything we do from now on is a direct response to one of those threats. Without that context, the code below is just another tutorial.&lt;/p&gt;

&lt;p&gt;In this part we are going to start writing the API. By the end you will have a Rails 8 project with user registration, login, and token-based authentication using OAuth2 + JWT, with tokens stored safely in &lt;code&gt;HttpOnly&lt;/code&gt; cookies instead of &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I want to be honest about something. When I first built this, I tried to do "everything at once". I added authentication, authorization, rate limiting, and serializers in the same commit, and I got lost. So in this series we are going slow on purpose. Part 2 is only about laying the foundation correctly. We will not finish every mitigation today, and that's fine.&lt;/p&gt;

&lt;p&gt;To help us stay oriented, I'll keep a small progress tracker at the end of each post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building in Part 2
&lt;/h2&gt;

&lt;p&gt;A small Rails 8 API with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;User&lt;/code&gt; model with hashed passwords (no plain text, ever)&lt;/li&gt;
&lt;li&gt;OAuth2 password grant flow so the client can exchange email + password for a token&lt;/li&gt;
&lt;li&gt;JWT access tokens that the server can verify without hitting the database&lt;/li&gt;
&lt;li&gt;Refresh tokens with short-lived access tokens&lt;/li&gt;
&lt;li&gt;Tokens delivered through encrypted &lt;code&gt;HttpOnly&lt;/code&gt; cookies, not JSON bodies the frontend has to store manually&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've never touched Devise or Doorkeeper before, don't worry. I'll explain &lt;em&gt;why&lt;/em&gt; we use each piece, not just &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ruby on Rails 8&lt;/li&gt;
&lt;li&gt;PostgreSQL 14+&lt;/li&gt;
&lt;li&gt;Postman, Insomnia, or &lt;code&gt;curl&lt;/code&gt; for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1. Create the project in API mode
&lt;/h2&gt;

&lt;p&gt;Rails has a built-in flag to skip all the browser-only middleware (cookies are gone by default, ERB views are gone, asset pipeline is gone). That's what we want for an API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new secure_api_auth &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; postgresql &lt;span class="nt"&gt;--api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking that down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-T&lt;/code&gt; skips the default test suite (we'll set up testing later in the series)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-d postgresql&lt;/code&gt; uses Postgres instead of SQLite&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--api&lt;/code&gt; strips out browser-oriented middleware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;secure_api_auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip from experience:&lt;/strong&gt; commit right here as "Initial commit" before you change anything. When something breaks two hours from now, having a clean baseline to &lt;code&gt;git diff&lt;/code&gt; against will save you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2. Add the gems we need
&lt;/h2&gt;

&lt;p&gt;Open your &lt;code&gt;Gemfile&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="c1"&gt;# Password hashing&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'bcrypt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 3.1.7'&lt;/span&gt;

&lt;span class="c1"&gt;# Authentication &amp;amp; Authorization&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'devise'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'doorkeeper'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'doorkeeper-jwt'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;A quick mental model before we go further, because these three gems confused me for a long time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Devise&lt;/strong&gt; owns the &lt;em&gt;user identity&lt;/em&gt;. It knows how to store a user, hash a password, and verify "is this the right password for this email?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Doorkeeper&lt;/strong&gt; owns the &lt;em&gt;access decision&lt;/em&gt;. After Devise confirms who you are, Doorkeeper issues the token that proves you are allowed to call the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;doorkeeper-jwt&lt;/strong&gt; changes the &lt;em&gt;format&lt;/em&gt; of that token from a random string to a JWT, so the server can verify it without a database lookup.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words: Devise checks the ID at the door, Doorkeeper hands you the wristband, and JWT is what the wristband is made of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Install Devise
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate devise:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Devise will print a few setup instructions. Since we're API-only, we only care about one of them: setting the default URL options for development.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;config/environments/development.rb&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;port: &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We won't be sending real emails in this tutorial, but Devise complains if this isn't set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate the User model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate devise User
bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;users&lt;/code&gt; table with an &lt;code&gt;encrypted_password&lt;/code&gt; column (and a few others for tracking sign-in counts and lockouts).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛡️ Mitigation in action: Token Theft and Password Breaches (Part 1, vectors 4 and 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Devise hashes passwords with &lt;strong&gt;bcrypt&lt;/strong&gt;, which is intentionally slow. Even if an attacker dumps your database, they can't reverse the hashes. Each password also gets a unique salt, so two users who pick the same password end up with completely different hashes. This is why we never, ever store passwords in plain text.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Switch Devise to API mode
&lt;/h3&gt;

&lt;p&gt;By default Devise wants to redirect users to HTML pages after login. We need to turn that off.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;config/initializers/devise.rb&lt;/code&gt; and add (or modify) these two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Devise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# ... keep everything else as generated ...&lt;/span&gt;

  &lt;span class="c1"&gt;# Don't use session storage for API authentication&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skip_session_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:http_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:params_auth&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# No HTML redirects, we return JSON&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigational_formats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4. Install Doorkeeper
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate doorkeeper:install
bin/rails generate doorkeeper:migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we run that migration, we need to edit it. Doorkeeper is built for the &lt;em&gt;full&lt;/em&gt; OAuth2 flow (the one where you click "Log in with GitHub" and get redirected). We don't need that. We need the simpler &lt;strong&gt;password grant&lt;/strong&gt; flow, where the client sends email + password directly and gets a token back. That requires loosening two &lt;code&gt;null: false&lt;/code&gt; constraints.&lt;/p&gt;

&lt;p&gt;Open the generated migration file (something like &lt;code&gt;db/migrate/XXXXX_create_doorkeeper_tables.rb&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In the oauth_applications table: allow null redirect_uri&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="ss"&gt;:redirect_uri&lt;/span&gt;  &lt;span class="c1"&gt;# remove the `null: false`&lt;/span&gt;

&lt;span class="c1"&gt;# In the oauth_access_tokens table: allow null application reference&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:application&lt;/span&gt;  &lt;span class="c1"&gt;# remove the `null: false`&lt;/span&gt;

&lt;span class="c1"&gt;# At the bottom of the file, link tokens back to users&lt;/span&gt;
&lt;span class="n"&gt;add_foreign_key&lt;/span&gt; &lt;span class="ss"&gt;:oauth_access_grants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: :resource_owner_id&lt;/span&gt;
&lt;span class="n"&gt;add_foreign_key&lt;/span&gt; &lt;span class="ss"&gt;:oauth_access_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: :resource_owner_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then migrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A word on the password grant flow.&lt;/strong&gt; The OAuth2 spec actually discourages this flow for third-party clients, because it requires the client to handle the user's raw password. But for &lt;strong&gt;first-party&lt;/strong&gt; clients (your own mobile app, your own SPA), it's perfectly reasonable, and it's what most "log in with email and password" APIs do under the hood. The key word is &lt;em&gt;first-party&lt;/em&gt;: you control both ends.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5. Re-enable cookies in API mode
&lt;/h2&gt;

&lt;p&gt;This is the part that surprised me the first time. When you pass &lt;code&gt;--api&lt;/code&gt; to &lt;code&gt;rails new&lt;/code&gt;, Rails removes the cookie middleware. That makes sense, an API doesn't usually need cookies. But we &lt;em&gt;do&lt;/em&gt; want them, because we want to store the access token in an &lt;code&gt;HttpOnly&lt;/code&gt; cookie instead of letting JavaScript handle it.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;SecureApiAuth&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_defaults&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="c1"&gt;# Re-add cookie middleware so we can use HttpOnly cookie auth&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cookies&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CookieStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s1"&gt;'_secure_api_session'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;same_site: :lax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;secure: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;production?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛡️ Mitigation in action: XSS and Token Theft (Part 1, vectors 1 and 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the single most important decision in this whole post. If you store a JWT in &lt;code&gt;localStorage&lt;/code&gt;, any piece of JavaScript that runs on your page can read it. One XSS bug, one compromised npm dependency, one browser extension, and the token is gone.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HttpOnly&lt;/code&gt; cookies are invisible to JavaScript. The browser sends them automatically with every request, but &lt;code&gt;document.cookie&lt;/code&gt; will not show them. &lt;code&gt;Secure&lt;/code&gt; ensures the cookie is only sent over HTTPS. &lt;code&gt;SameSite=Lax&lt;/code&gt; is our first line of defense against CSRF (which we'll fully address in a later part).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 6. Teach Doorkeeper to read tokens from the cookie
&lt;/h2&gt;

&lt;p&gt;Out of the box, Doorkeeper looks for tokens in the &lt;code&gt;Authorization: Bearer ...&lt;/code&gt; header. We need to add a new place for it to look: our encrypted cookie.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;lib/doorkeeper/from_cookie.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Doorkeeper&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FromCookie&lt;/span&gt;
    &lt;span class="kp"&gt;module_function&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# Read the encrypted access token from the HttpOnly cookie&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie_jar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:access_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;encrypted&lt;/code&gt; and not just &lt;code&gt;signed&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rails offers two protected cookie jars: &lt;code&gt;signed&lt;/code&gt; (tamper-proof but readable) and &lt;code&gt;encrypted&lt;/code&gt; (tamper-proof AND unreadable). If someone opens DevTools and copies the raw cookie value, with &lt;code&gt;signed&lt;/code&gt; they'd see the JWT in plain text. With &lt;code&gt;encrypted&lt;/code&gt; they see AES-256 garbage. The JWT is already protected by its own signature, but defense in depth is cheap here, so we use &lt;code&gt;encrypted&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 7. Configure Doorkeeper
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;config/initializers/doorkeeper.rb&lt;/code&gt; and replace the generated content with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'../../lib/doorkeeper/from_cookie'&lt;/span&gt;

&lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;orm&lt;/span&gt; &lt;span class="ss"&gt;:active_record&lt;/span&gt;
  &lt;span class="n"&gt;api_only&lt;/span&gt;

  &lt;span class="c1"&gt;# Resource Owner Password Credentials grant.&lt;/span&gt;
  &lt;span class="c1"&gt;# Lets users log in with email + password directly.&lt;/span&gt;
  &lt;span class="n"&gt;grant_flows&lt;/span&gt; &lt;span class="sx"&gt;%w[password]&lt;/span&gt;
  &lt;span class="n"&gt;allow_blank_redirect_uri&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# How to find the user when they log in.&lt;/span&gt;
  &lt;span class="n"&gt;resource_owner_from_credentials&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_routes&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_for_database_authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid_password?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;skip_client_authentication_for_password_grant&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Short-lived access tokens.&lt;/span&gt;
  &lt;span class="n"&gt;access_token_expires_in&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

  &lt;span class="c1"&gt;# Enable refresh tokens.&lt;/span&gt;
  &lt;span class="n"&gt;use_refresh_token&lt;/span&gt;

  &lt;span class="c1"&gt;# Use JWT format for access tokens.&lt;/span&gt;
  &lt;span class="n"&gt;access_token_generator&lt;/span&gt; &lt;span class="s1"&gt;'Doorkeeper::JWT'&lt;/span&gt;

  &lt;span class="c1"&gt;# Scopes. We'll use these later for authorization.&lt;/span&gt;
  &lt;span class="n"&gt;default_scopes&lt;/span&gt; &lt;span class="ss"&gt;:read&lt;/span&gt;
  &lt;span class="n"&gt;optional_scopes&lt;/span&gt; &lt;span class="ss"&gt;:write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:admin&lt;/span&gt;

  &lt;span class="n"&gt;base_controller&lt;/span&gt; &lt;span class="s1"&gt;'ApplicationController'&lt;/span&gt;

  &lt;span class="c1"&gt;# Token lookup order: 1) our cookie, 2) Bearer header, 3) query param.&lt;/span&gt;
  &lt;span class="n"&gt;access_token_methods&lt;/span&gt; &lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FromCookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="ss"&gt;:from_bearer_authorization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="ss"&gt;:from_access_token_param&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few of these settings deserve a closer look:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;access_token_expires_in 15.minutes&lt;/code&gt;&lt;/strong&gt; is intentional. If a token leaks, the attacker only has 15 minutes before it stops working. The refresh token (which lives longer) covers the user experience side, so they don't have to log in every 15 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;use_refresh_token&lt;/code&gt;&lt;/strong&gt; enables the rotation pattern: when an access token expires, the client uses a refresh token to get a new one without prompting the user. We'll wire up rotation more carefully in a later part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;access_token_methods&lt;/code&gt;&lt;/strong&gt; order matters here. Doorkeeper checks the cookie first, then the &lt;code&gt;Authorization&lt;/code&gt; header, then a query parameter. In production I'd actually remove &lt;code&gt;from_access_token_param&lt;/code&gt; because tokens in URLs end up in server logs and browser history (Part 1, vector 10). For now I'm leaving it in so you can test easily with curl, but treat it as a TODO.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛡️ Mitigation in action: Token Theft (Part 1, vector 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Short access token lifetime plus refresh token rotation is the standard pattern for limiting blast radius when a token leaks. We'll harden this further by adding revocation when we build the &lt;code&gt;/logout&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8. Configure the JWT payload
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;config/initializers/doorkeeper_jwt.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Sign tokens with HS256 using the app's secret key.&lt;/span&gt;
  &lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;secret_key_base&lt;/span&gt;
  &lt;span class="n"&gt;signing_method&lt;/span&gt; &lt;span class="ss"&gt;:hs256&lt;/span&gt;

  &lt;span class="n"&gt;token_payload&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:resource_owner_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;iat: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# Issued at&lt;/span&gt;
      &lt;span class="ss"&gt;exp: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:expires_in&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Expires at&lt;/span&gt;
      &lt;span class="ss"&gt;jti: &lt;/span&gt;&lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# Unique token ID&lt;/span&gt;
      &lt;span class="ss"&gt;sub: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;# Subject (user ID)&lt;/span&gt;
      &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:scopes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;secret_key_path&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="n"&gt;use_application_secret&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've never seen a JWT before, here's the short version. A JWT is three base64-encoded parts joined by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header says how the token is signed. The payload is the JSON we just defined above (user ID, email, expiry, etc). The signature is a hash of &lt;code&gt;header.payload&lt;/code&gt; combined with our secret key. If an attacker changes anything in the payload, the signature won't match anymore and the token is rejected.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jti&lt;/code&gt; (JWT ID) is worth flagging. It's a unique ID per token, which we'll use later when we implement token revocation. You "blacklist" the &lt;code&gt;jti&lt;/code&gt; instead of trying to delete the JWT itself (you can't, the client has it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we are right now
&lt;/h2&gt;

&lt;p&gt;We have a Rails 8 API with a &lt;code&gt;User&lt;/code&gt; model, password hashing, OAuth2 password grant, JWT access tokens, refresh tokens, and &lt;code&gt;HttpOnly&lt;/code&gt; cookie storage. That's a solid foundation.&lt;/p&gt;

&lt;p&gt;But we have &lt;strong&gt;not&lt;/strong&gt; written the controllers yet (register, login, logout, refresh, "who am I"). That's coming in Part 3, along with the first set of mitigations that depend on having endpoints to protect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progress tracker: security vectors from Part 1
&lt;/h2&gt;

&lt;p&gt;I'll keep this table updated in every part so you can see exactly what's covered and what's still pending.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Attack vector&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;🟡 Partially mitigated&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HttpOnly&lt;/code&gt; cookie storage (Step 5). Full content sanitization is a frontend concern.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SQL Injection&lt;/td&gt;
&lt;td&gt;🟢 Mitigated by default&lt;/td&gt;
&lt;td&gt;Active Record's &lt;code&gt;find_by&lt;/code&gt;, &lt;code&gt;where&lt;/code&gt;, etc. We'll still review controllers in Part 3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SameSite=Lax&lt;/code&gt; is set, but we need explicit CSRF tokens for cookie auth. Part 3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Brute Force&lt;/td&gt;
&lt;td&gt;🟡 Partially mitigated&lt;/td&gt;
&lt;td&gt;bcrypt slows password cracking. Rate limiting with &lt;code&gt;Rack::Attack&lt;/code&gt; comes in Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;User Enumeration&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;We need to design login/reset responses to be uniform. Part 3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;IDOR&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;Will be addressed when we add resources + Pundit/Sqids. Part 5.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Mass Assignment&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;Strong params, controller by controller. Part 3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Excessive Data Exposure&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;Serializers (JSON:API or &lt;code&gt;alba&lt;/code&gt;). Part 3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;MITM&lt;/td&gt;
&lt;td&gt;🟡 Partially mitigated&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;secure: true&lt;/code&gt; on cookies in production. &lt;code&gt;force_ssl&lt;/code&gt; and HSTS come in Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Token Theft&lt;/td&gt;
&lt;td&gt;🟢 Mostly mitigated&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HttpOnly&lt;/code&gt; + encrypted cookies + short-lived tokens + refresh tokens. Revocation in Part 3.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Verbose Error Messages&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;Production error handling. Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Legend: 🟢 Covered, 🟡 Partial, 🔴 Pending&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming up in Part 3
&lt;/h2&gt;

&lt;p&gt;We'll build the actual auth controllers (&lt;code&gt;register&lt;/code&gt;, &lt;code&gt;login&lt;/code&gt;, &lt;code&gt;logout&lt;/code&gt;, &lt;code&gt;refresh&lt;/code&gt;, and &lt;code&gt;me&lt;/code&gt;), and while we do it we'll knock out four more vectors from the tracker: &lt;strong&gt;CSRF&lt;/strong&gt;, &lt;strong&gt;User Enumeration&lt;/strong&gt;, &lt;strong&gt;Mass Assignment&lt;/strong&gt;, and &lt;strong&gt;Excessive Data Exposure&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If this helped you, follow along so you don't miss it. And if anything is unclear, drop a comment, I read all of them.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>api</category>
      <category>owasp</category>
    </item>
    <item>
      <title>Build a Secure API with Rails 8 - Part-1</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Wed, 06 May 2026 23:14:19 +0000</pubDate>
      <link>https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-1-11lh</link>
      <guid>https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-1-11lh</guid>
      <description>&lt;p&gt;Hi folks👋! &lt;/p&gt;

&lt;p&gt;In this post I want to share something I wish I had when I started building APIs with Ruby on Rails: a practical guide that takes security seriously from the beginning.&lt;/p&gt;

&lt;p&gt;When I built my first REST API, most tutorials I found were focused on getting something running quickly. They were great for learning the basics, but they usually skipped important topics like API versioning, authentication strategy, authorization, and security.&lt;/p&gt;

&lt;p&gt;Even when using AI tools to generate a “secure API”, the result is often still insecure unless you already understand the threats you are trying to protect against. Security is not something you get automatically. You need to know what problems you are solving and why the protections matter.&lt;/p&gt;

&lt;p&gt;I ended up reading API design books, OWASP documentation, and real-world breach reports before I finally felt like I understood what I was building, I've put all in practice. This post is the guide I wish I had back then.&lt;/p&gt;

&lt;p&gt;In this series we are going to build a production-ready Rails 8 API with authentication, authorization, rate limiting, secure cookies, security headers, and other important protections. I also want to explain the reasoning behind each decision, not just copy-paste code without context.&lt;/p&gt;

&lt;p&gt;Before writing any code, let’s first understand the main attack vectors we need to defend against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack vectors we are defending against
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. XSS (Cross-Site Scripting)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
XSS happens when an attacker injects malicious JavaScript into content that later gets rendered in another user’s browser. In API-driven applications, one of the biggest risks is token theft. If JWTs are stored in &lt;code&gt;localStorage&lt;/code&gt;, a malicious script can read and steal them immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Avoid storing authentication tokens in &lt;code&gt;localStorage&lt;/code&gt; or other browser-accessible storage. Instead, store them in secure &lt;code&gt;HttpOnly&lt;/code&gt; cookies so JavaScript cannot access them. Cookies should also use the &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;SameSite&lt;/code&gt; attributes. Any user-generated content rendered in the frontend should be properly escaped or sanitized.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. SQL Injection
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
SQL Injection happens when user input is inserted directly into a SQL query without proper sanitization. An attacker can manipulate the query to bypass authentication, read sensitive data, or modify the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Avoid interpolating user input directly into SQL queries. In Rails, prefer Active Record methods like &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;find_by&lt;/code&gt;, and parameterized queries, which automatically sanitize input. If raw SQL is unavoidable, use bound parameters instead of string interpolation. You should also validate input, use strong parameters, and follow the principle of least privilege for database accounts.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. CSRF (Cross-Site Request Forgery)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
CSRF happens when a malicious website tricks a logged-in user’s browser into sending authenticated requests to your application using automatically attached cookies.&lt;/p&gt;

&lt;p&gt;This is especially important in Rails APIs using session cookies or JWTs stored in &lt;code&gt;HttpOnly&lt;/code&gt; cookies. Even though JavaScript cannot read those cookies, the browser still sends them automatically with requests.&lt;/p&gt;

&lt;p&gt;An attacker could potentially trigger actions like changing account settings, creating resources, or deleting data without the user realizing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Enable CSRF protection for any cookie-based authentication flow. In Rails, use &lt;code&gt;protect_from_forgery&lt;/code&gt; and require valid CSRF tokens for state-changing requests like &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Authentication cookies should also use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;HttpOnly&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Secure&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt; or &lt;code&gt;SameSite=Strict&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also validate &lt;code&gt;Origin&lt;/code&gt; and &lt;code&gt;Referer&lt;/code&gt; headers and keep CORS restricted to trusted frontend domains.&lt;/p&gt;

&lt;p&gt;If the browser automatically sends authentication, CSRF protection still matters, even if the API itself is technically stateless.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Brute Force
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Brute force attacks happen when an attacker repeatedly tries large numbers of username and password combinations against your login endpoint.&lt;/p&gt;

&lt;p&gt;This commonly targets login forms, password reset endpoints, and authentication APIs. Successful attacks can lead to account compromise, credential stuffing, and unnecessary server load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Use rate limiting on authentication-related endpoints. In Rails, tools like &lt;code&gt;Rack::Attack&lt;/code&gt; can throttle repeated requests by IP address, email, or both.&lt;/p&gt;

&lt;p&gt;You should also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;temporarily lock accounts after repeated failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;require strong passwords&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;detect suspicious login activity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;avoid revealing whether an account exists&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;consider CAPTCHA or step-up verification after suspicious behavior&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  5. User Enumeration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
User enumeration happens when an application reveals whether an account exists through different error messages.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Email not found”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Incorrect password”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An attacker can use these differences to discover valid accounts and later target them with brute force attacks, phishing, or credential stuffing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Return consistent responses during login, password reset, and account recovery flows.&lt;/p&gt;

&lt;p&gt;Instead of exposing whether the email exists, use generic responses such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Invalid credentials”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“If an account exists, instructions have been sent”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also rate limit these endpoints and monitor repeated probing attempts.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. IDOR (Insecure Direct Object Reference)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
IDOR happens when users can access resources they do not own by changing identifiers in URLs or request parameters.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ownership checks are missing, changing &lt;code&gt;/users/42&lt;/code&gt; to &lt;code&gt;/users/43&lt;/code&gt; could expose another user’s data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Always scope records through the authenticated user or an authorization policy.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authorization libraries like Pundit or CanCanCan also help enforce access rules consistently across the application. I also avoid exposing raw database IDs directly to the frontend. Instead, I use &lt;a href="https://sqids.org/ruby" rel="noopener noreferrer"&gt;Sqids&lt;/a&gt;￼to generate less predictable public IDs, which helps reduce simple enumeration attacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Mass Assignment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Mass assignment happens when the application accepts user input and blindly assigns it to model attributes.&lt;/p&gt;

&lt;p&gt;An attacker could submit unexpected fields such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those fields are not filtered properly, the attacker may gain elevated privileges or modify protected data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Use strong parameters in every controller.&lt;/p&gt;

&lt;p&gt;In Rails, always whitelist allowed attributes using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never pass raw &lt;code&gt;params&lt;/code&gt; directly into &lt;code&gt;create&lt;/code&gt; or &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sensitive fields like roles, permissions, ownership fields, or account status flags should never be user-assignable.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Excessive Data Exposure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Excessive data exposure happens when an API returns more information than the client actually needs.&lt;/p&gt;

&lt;p&gt;This often happens when entire Active Record objects are rendered directly into JSON responses.&lt;/p&gt;

&lt;p&gt;Sensitive data such as password digests, internal IDs, permissions, API keys, or private metadata may accidentally leak through the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Only return the fields the client actually needs.&lt;/p&gt;

&lt;p&gt;Instead of blindly rendering full objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use serializers or custom JSON responses that explicitly define safe attributes.&lt;/p&gt;

&lt;p&gt;Sensitive fields should never appear in API responses.&lt;/p&gt;

&lt;p&gt;You should also regularly review serialized responses to make sure no internal data is leaking unintentionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. MITM (Man-in-the-Middle)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
A Man-in-the-Middle attack happens when an attacker intercepts traffic between the client and server.&lt;/p&gt;

&lt;p&gt;Without HTTPS, credentials, tokens, cookies, and other sensitive data can travel in plain text and be stolen or modified.&lt;/p&gt;

&lt;p&gt;Attackers on the same network, malicious proxies, or compromised routers can hijack sessions or impersonate users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Always enforce HTTPS.&lt;/p&gt;

&lt;p&gt;In Rails, enable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;force_ssl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This redirects insecure requests and ensures cookies are only sent over encrypted connections.&lt;/p&gt;

&lt;p&gt;Authentication cookies should also use the &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;HttpOnly&lt;/code&gt; flags.&lt;/p&gt;

&lt;p&gt;You should additionally enable HSTS headers and avoid loading insecure mixed-content resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Token Theft
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Token theft happens when an attacker gains access to a valid authentication token and uses it to impersonate a user.&lt;/p&gt;

&lt;p&gt;Stolen JWTs can come from XSS attacks, insecure storage, leaked logs, browser extensions, compromised devices, or intercepted traffic.&lt;/p&gt;

&lt;p&gt;If tokens remain valid for a long time, the attacker may keep access even after the user notices something is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Reduce token exposure and keep token lifetimes short.&lt;/p&gt;

&lt;p&gt;Prefer storing tokens in secure &lt;code&gt;HttpOnly&lt;/code&gt; cookies instead of &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;short-lived access tokens&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;refresh token rotation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;token revocation mechanisms&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also avoid exposing tokens in logs or URLs and protect the application against XSS vulnerabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Verbose Error Messages
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Verbose error messages expose internal application details to attackers.&lt;/p&gt;

&lt;p&gt;Stack traces, database errors, framework versions, SQL queries, and file paths can all help attackers understand how the system works and make exploitation easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Production applications should return generic and safe error responses.&lt;/p&gt;

&lt;p&gt;Instead of exposing internal exceptions, return messages such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Internal Server Error&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Invalid request&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detailed errors should only be logged internally for debugging.&lt;/p&gt;

&lt;p&gt;In Rails, make sure debug pages and detailed exceptions are disabled in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;These are some of the most important security risks to think about when building APIs, and we will revisit them throughout this series as we implement each feature step by step.&lt;/p&gt;

&lt;p&gt;In Part 2 we will start building the Rails 8 API from scratch and set up the project foundation correctly from the beginning, including authentication, secure configuration, and API structure.&lt;/p&gt;

&lt;p&gt;Follow along if you want to get notified when the next part is published.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>api</category>
      <category>ruby</category>
      <category>owasp</category>
    </item>
    <item>
      <title>What are my day-to-day tools as a full-stack developer?</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Sat, 20 Jun 2020 00:26:50 +0000</pubDate>
      <link>https://dev.to/renzodiaz/what-are-my-day-to-day-tools-as-a-full-stack-developer-1lan</link>
      <guid>https://dev.to/renzodiaz/what-are-my-day-to-day-tools-as-a-full-stack-developer-1lan</guid>
      <description>&lt;p&gt;I've divided a list of tools that I use every day working as a full-stack developer by category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Graphic &lt;a href="https://www.graphic.com/"&gt;link&lt;/a&gt;:
&lt;/h4&gt;

&lt;p&gt;I can't afford Illustrator monthly so I found this alternative, I paid once and it worth all. I can edit vectors, AI files, export everything that you can do with Illustrator.&lt;/p&gt;

&lt;h4&gt;
  
  
  SketchApp &lt;a href="https://www.sketch.com/"&gt;link&lt;/a&gt;:
&lt;/h4&gt;

&lt;p&gt;When it comes to design mockups I use Sketch, it has a friendly interface that makes it easy to design.&lt;/p&gt;

&lt;h4&gt;
  
  
  Zeplin &lt;a href="https://zeplin.io/"&gt;link&lt;/a&gt;:
&lt;/h4&gt;

&lt;p&gt;If you are developing an application you should have quick access to the mockup assets like images, colors, icons, etc. Zeplin is a design collaboration tool that allows uploading your mockups to the cloud you all the development team can access them quickly and easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Iterm2 &lt;a href="https://www.iterm2.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;An alternative to default terminal on macOS&lt;/p&gt;

&lt;h4&gt;
  
  
  Vim &lt;a href="https://www.vim.org/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;I use Vim as the code editor, I've customized shortcuts and plugins for fast development. &lt;/p&gt;

&lt;h4&gt;
  
  
  Docker &lt;a href="https://www.docker.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Docker to containerize my apps&lt;/p&gt;

&lt;h4&gt;
  
  
  Xcode&lt;a href="https://developer.apple.com/xcode/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;When I need to develop an IOS app I use Xcode with Swift.&lt;/p&gt;

&lt;h4&gt;
  
  
  TMUX &lt;a href="https://github.com/tmux/tmux/wiki"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;When I'm developing a web application I need quick access to different terminals, for example, to run a server, to use my Vim editor all in just one terminal in a single session.&lt;/p&gt;

&lt;h4&gt;
  
  
  Homebrew &lt;a href="https://brew.sh/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;A package manager for macOS.&lt;/p&gt;

&lt;h4&gt;
  
  
  Repl &lt;a href="https://repl.it"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Sometimes I need to write quick methods to see what returns on the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Gitlab &lt;a href="https://gitlab.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;I'm a fan of Gitlab, private repositories, CI/CD and you can also use the community edition to install in your own server.&lt;/p&gt;

&lt;h4&gt;
  
  
  HEROKU &lt;a href="https://heroku.com"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Where I deploy my personal projects.&lt;/p&gt;

&lt;h4&gt;
  
  
  AWS S3 &lt;a href="https://aws.amazon.com/s3/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;When I'm working on a big application I store the resources in a bucket in AWS S3, and when I need to create a CDN as well.&lt;/p&gt;

&lt;h4&gt;
  
  
  PivotalTracker &lt;a href="https://www.pivotaltracker.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Every developer should use a tool to track progress on the development or specific task, I use PivotalTracker for big projects and Gitlab board for small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Utilities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1Password: My generate and store passwords.&lt;/li&gt;
&lt;li&gt;Boostnote: Save my snippets or documentation.&lt;/li&gt;
&lt;li&gt;Workspace: I organize my apps in workspaces, so I can access them in one click.&lt;/li&gt;
&lt;li&gt;Toggle Alfred: Enhanced spotlight in mac.&lt;/li&gt;
&lt;li&gt;ExpressVPN: A VPN to navigate safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not a Rich guy that can afford a Mac, I use a Mac because I feel more comfortable work design, develop Web and iOS applications. I work for years to get a MacBook 2017 before I used to go to my university to use design tools.&lt;/p&gt;

&lt;p&gt;I would appreciate it if you can share the tools you use in your day-to-day.&lt;/p&gt;

&lt;p&gt;Have a nice day!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to use ULID as primary key Rails</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Thu, 21 May 2020 19:54:29 +0000</pubDate>
      <link>https://dev.to/renzodiaz/how-to-use-ulid-as-primary-key-rails-5hf6</link>
      <guid>https://dev.to/renzodiaz/how-to-use-ulid-as-primary-key-rails-5hf6</guid>
      <description>&lt;h3&gt;
  
  
  A short story
&lt;/h3&gt;

&lt;p&gt;Ruby on Rails is a powerful framework it is easy to create APIs and fast website development, one thing that came when I was developing an API was how to change the &lt;em&gt;Integer auto-increment primary key&lt;/em&gt; because having a predictable Id can be risky in terms of security if you share it for a client-size application, the user can guess ids and modify the database in the worst of the case, at that moment I had to research some alternatives and I found &lt;a href="https://en.wikipedia.org/wiki/Universally_unique_identifier"&gt;UUID&lt;/a&gt; a non-sequential data type that can be easily be implemented in Rails, so I've tried it and everything was ok until I had to sort &lt;em&gt;ASC&lt;/em&gt; and &lt;em&gt;DESC&lt;/em&gt;, I realize that the methods &lt;code&gt;Model.first&lt;/code&gt; and &lt;code&gt;Model.last&lt;/code&gt; doesn't work as expected because Rails by default uses them with the &lt;em&gt;integer sequential id&lt;/em&gt; type, so as &lt;strong&gt;UUID&lt;/strong&gt; is non-sequential it generates a random Hash like this &lt;code&gt;123e4567-e89b-12d3-a456-426614174000&lt;/code&gt; as it doesn't have a sequence Rails can't figure out how to sort the data, then I researched a little bit more to see if there is any other solution and I found that we can use &lt;code&gt;self.implicit_order_column = "created_at"&lt;/code&gt; in the Model, with this, those methods work ok it sorts depending on the &lt;em&gt;DateTime&lt;/em&gt; created and I was happy. Not for a long time, when another problem came up! When I used &lt;em&gt;seeds&lt;/em&gt; to fill a bulk of fake data into the database, all the data had the same &lt;em&gt;DateTime&lt;/em&gt; and the sorting failed again, it was the same issue as the initial one. I've decided to look for another alternative and I found &lt;a href="https://github.com/ulid/spec"&gt;ULID&lt;/a&gt;, using this I had everything I've needed the sorting works as expected, it isn't predictable and the store in memory is pretty good. I'm still using it and I don't have any issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to implement ULID in Rails?
&lt;/h3&gt;

&lt;p&gt;There is already a &lt;a href="https://github.com/rafaelsales/ulid"&gt;gem&lt;/a&gt; for ruby add it to your &lt;code&gt;Gemfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#Gemfile&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'ulid'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;bundle install&lt;/code&gt; to install it. Then update your migration or if you are creating a new one should look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# migration&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUsers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
   &lt;span class="c1"&gt;# id: false, to not use id int as default&lt;/span&gt;
   &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
     &lt;span class="c1"&gt;# here we create our id&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binary&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:given_name&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:family_name&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:password_digest&lt;/span&gt;
     &lt;span class="o"&gt;...&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;First, we disable the autogenerated id by putting &lt;code&gt;id: false&lt;/code&gt; and create our own id &lt;code&gt;t.binary :id, limit: 16, primary_key: true&lt;/code&gt; what we need to do is fill the id before create, we can do it on each Model but my approach was to create a &lt;em&gt;concern&lt;/em&gt; to just include it in the &lt;code&gt;application_record.rb&lt;/code&gt; file so it will apply for all the models.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/concenrs/ulid_pk.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'ulid'&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;UlidPk&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;before_create&lt;/span&gt; &lt;span class="ss"&gt;:set_ulid&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_ulid&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ULID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then in the &lt;code&gt;application_record.rb&lt;/code&gt; include it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/concenrs/ulid_pk.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationRecord&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;UlidPk&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abstract_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That's it, now it will autogenerate the id before create.&lt;/p&gt;

&lt;h3&gt;
  
  
  Note
&lt;/h3&gt;

&lt;p&gt;if you need to create an association you should put the type on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# migration&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateEvents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
   &lt;span class="c1"&gt;# id: false, to not use id int as default&lt;/span&gt;
   &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
     &lt;span class="c1"&gt;# here we create our id&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binary&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;
     &lt;span class="o"&gt;...&lt;/span&gt;
     &lt;span class="c1"&gt;# Specify the type: binary&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Hope this can help you, I'll try to make it more autogenerated when I get the chance to avoid put &lt;code&gt;type: :binary&lt;/code&gt; or &lt;code&gt;id: false&lt;/code&gt; manually. If you have any question feel free to reach me out. Cheers!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/renzodiaz"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PT2F9GG6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>postgres</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Architect Rails Project - Part 2</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Sun, 08 Dec 2019 21:15:59 +0000</pubDate>
      <link>https://dev.to/renzodiaz/how-to-architect-rails-project-part-2-3be8</link>
      <guid>https://dev.to/renzodiaz/how-to-architect-rails-project-part-2-3be8</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t2sMaH5V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%252Cf_auto%252Cfl_progressive%252Cq_auto%252Cw_880/https://miro.medium.com/max/1060/1%252ArW0W8dEaRtJzuXRz3RCszA.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t2sMaH5V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%252Cf_auto%252Cfl_progressive%252Cq_auto%252Cw_880/https://miro.medium.com/max/1060/1%252ArW0W8dEaRtJzuXRz3RCszA.jpeg" alt="Modules"&gt;&lt;/a&gt;&lt;br&gt;
In the previous article, we've done setting up our Surface module&lt;br&gt;
&lt;a href="https://dev.to/renzodiaz/how-to-architect-rails-project-part-1-34bp"&gt;How to Architect Rails Project - Part 1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we'll cover the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Module Admin&lt;/li&gt;
&lt;li&gt;Layouts&lt;/li&gt;
&lt;li&gt;Front-End Scaffolding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, let's kick off...&lt;/p&gt;

&lt;p&gt;For our Admin module, we want to get these routes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="s1"&gt;'domain.com/admins'&lt;/span&gt; &lt;span class="c1"&gt;# Overview, basic reports, charts&lt;/span&gt;
&lt;span class="s1"&gt;'domain.com/admins/blog'&lt;/span&gt; &lt;span class="c1"&gt;# Articles CRUD&lt;/span&gt;
&lt;span class="s1"&gt;'domain.com/admins/work'&lt;/span&gt; &lt;span class="c1"&gt;# Work CRUD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's add them to our routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:admins&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:blogs&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:works&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Why &lt;strong&gt;admins&lt;/strong&gt; and not &lt;strong&gt;admin&lt;/strong&gt; in singular? Because this module was thought to have multiple user types, users that only can publish articles and won't have permissions to publish works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Moving on, we need to create the views for our Admin module according to our routes.&lt;/p&gt;

&lt;p&gt;Create these files inside &lt;code&gt;app/views&lt;/code&gt; folder&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/views
  ├── admins/blogs
  │   └── index.html.erb
  │   └── show.html.erb
  ├── admins/works
  │   └── index.html.erb
  │   └── show.html.erb
  └── layouts 
      └── admin.html.erb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's add our markup to &lt;code&gt;admin.html.erb&lt;/code&gt; layout, for now, let's copy the content from &lt;code&gt;application.html.erb&lt;/code&gt;. Our layout would look like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"partials/header"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Cool right? We have our Admin layout. &lt;/p&gt;

&lt;p&gt;Hold on your horses... We are still using the assets from our &lt;strong&gt;Surface&lt;/strong&gt; module (application), we want to isolate &lt;strong&gt;Admin/Surface&lt;/strong&gt; and make them to not depend on each other, so, let's isolate our Admin assets out of the Surface module.&lt;/p&gt;

&lt;p&gt;Create these files under &lt;code&gt;app/assets/stylesheets&lt;/code&gt;...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/assets/stylesheets
  ├── admin/base
  │   └── _base.scss
  │   └── _variables.scss
  ├── admin/components
  │   └── _header.scss
  │   └── _sidebar.scss
  └── admin.scss
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So our &lt;code&gt;admin.scss&lt;/code&gt; would look like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Base theming&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/base/variables"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/base/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Components&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/components/header"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/components/sidebar"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, add this line to our &lt;code&gt;base_controller.rb&lt;/code&gt; under &lt;code&gt;controller/admins&lt;/code&gt; folder&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admins::BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We need also to tell rails that we want to use &lt;code&gt;admin.css|admin.js&lt;/code&gt; in our initializer, so add the following line to &lt;code&gt;config/initializers/assets.rb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;precompile&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sx"&gt;%w( admin.js admin.css )&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now let's change our &lt;code&gt;admin.html.erb&lt;/code&gt; layout to use &lt;code&gt;admin.css&lt;/code&gt; and remove the current header stylesheet which comes from the &lt;code&gt;Surface&lt;/code&gt; module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Well done! now if we run &lt;code&gt;rails s&lt;/code&gt; and navigate to &lt;code&gt;http://localhost:3000/admins/blogs&lt;/code&gt; we should see a blank page. If we inspect elements in the browser we'll see that our styles are coming from &lt;strong&gt;admin&lt;/strong&gt; and not &lt;strong&gt;application&lt;/strong&gt; anymore.&lt;/p&gt;

&lt;p&gt;Alright, Renzo, this is cool but I don't see any component and styles on the page yet. Besides the javascript still comes from the Surface module.&lt;/p&gt;

&lt;p&gt;Before deep into the components and styles, let's add an Admin module for our javascript files, this is pretty much the same as what we did with styles.&lt;/p&gt;

&lt;p&gt;So, let's create an &lt;code&gt;admin.js&lt;/code&gt; file under &lt;code&gt;app/javascript/packs&lt;/code&gt; folder and add the following lines...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/packs/admin.js&lt;/span&gt;
&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@rails/ujs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbolinks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@rails/activestorage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;&amp;lt;%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;%= javascript_pack_tag 'admin', 'data-turbolinks-track': 'reload' %&amp;gt;&lt;/code&gt; in our &lt;code&gt;admin.html.erb&lt;/code&gt; layout&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's restart our server and see what we got, we should see the same blank page if we navigate to &lt;code&gt;http://localhost:3000/admins/blogs&lt;/code&gt;. If we inspect the page we'll see that now our javascript comes from admin.&lt;/p&gt;

&lt;p&gt;Now we have our js/css isolated from the Surface module, let's add Bulma to our project and start building our components.&lt;/p&gt;

&lt;p&gt;Add &lt;a href="https://github.com/joshuajansen/bulma-rails"&gt;Bulma gem&lt;/a&gt; to our &lt;code&gt;Gemfile&lt;/code&gt; at the bottom&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem "bulma-rails", "~&amp;gt; 0.8.0"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;bundle install&lt;/code&gt; in the terminal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this article we are going to use Bulma in both modules &lt;code&gt;Surface/Admin&lt;/code&gt;, just to not make this article too long. Remember that we can also isolate to use Bulma for the Admin and any other framework for our Surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now let's build our layout markup correctly for the Admin module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!Doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
...
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"patials/admin/header"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"columns is-fullheight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"column is-2 is-sidebar-menu is-hidden-mobile"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"partials/admin/sidebar"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"column is-main-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;admin&lt;/code&gt; folder under &lt;code&gt;app/views/partials&lt;/code&gt; add these files...&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/partials/admin/_header.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-brand"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-item"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Logo
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt; 
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-burger burger"&lt;/span&gt; &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"navMenubd-example"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"navMenubd-example"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-item has-dropdown is-hoverable"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;figure&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"avatar image is-32x32"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"is-rounded"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://bulma.io/images/placeholders/32x32.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
          username
        &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-dropdown is-right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Logout
          &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/views/partials/admin/_sidebar.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;aside&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    General
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Overview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admins_blogs_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Administration
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'My Work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admins_works_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'My Work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admins_blogs_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&amp;gt;&lt;/span&gt;Users&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Add some styles&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/base/_variables.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nv"&gt;$body-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Responsiveness&lt;/span&gt;
&lt;span class="c1"&gt;// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16&lt;/span&gt;
&lt;span class="nv"&gt;$tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;769px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 960px container + 40px;&lt;/span&gt;
&lt;span class="nv"&gt;$desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 1152px container + 40;&lt;/span&gt;
&lt;span class="nv"&gt;$widescreen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1192px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 1344px container + 40;&lt;/span&gt;
&lt;span class="nv"&gt;$fullhd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1384px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$orange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hsl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;83%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;57%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$orange&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$primary-invert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$white&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Link colors&lt;/span&gt;
&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$primary&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$link-invert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$white&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="mi"&gt;.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/base/_base.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.columns&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;.is-fullheight&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;.75rem&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;.75rem&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;.75rem&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stretch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nc"&gt;.column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;    
&lt;span class="nc"&gt;.is-main-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$grey-lighter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;  &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/components/_sidebar.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.is-sidebar-menu&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$grey-dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/components/_header.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.navbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.navbar-item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;.avatar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;.65rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nc"&gt;.is-rounded&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Alright, if we restart our browser at this point and navigate to &lt;code&gt;http://localhost:3000/admins/blogs&lt;/code&gt; our dashboard should look like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--23YuF5rx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1919/1%2AlP3nTaT-q9Wz1rHfPFcd3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--23YuF5rx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1919/1%2AlP3nTaT-q9Wz1rHfPFcd3g.png" alt="Portfolio | Admin"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you got any error through this setup, feel free to drop me a line &lt;a href="mailto:dev.renzo.diaz@gmail.com"&gt;dev.renzo.diaz@gmail.com&lt;/a&gt; I’ll try to answer all your doubts asap.&lt;/p&gt;

&lt;p&gt;In part 1 of this article, I mentioned this part 2 will cover CRUDS and Authentication, and React as well.&lt;/p&gt;

&lt;p&gt;We'll, we will cover those points in part3, I don't want to make this article too long and don't mix with Rails back-end stuff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/renzodiaz"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PT2F9GG6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tutorial</category>
      <category>ruby</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Architect Rails Project - Part 1</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Wed, 04 Dec 2019 16:31:57 +0000</pubDate>
      <link>https://dev.to/renzodiaz/how-to-architect-rails-project-part-1-34bp</link>
      <guid>https://dev.to/renzodiaz/how-to-architect-rails-project-part-1-34bp</guid>
      <description>&lt;p&gt;When I started to build web applications with ruby on rails, I had a lot of questions that I figured out through the years of using the framework. Now in this article, I’m going to share the base structure to start building a web application.&lt;/p&gt;

&lt;p&gt;Depending on the requirements our structure could slightly change, in this scenario, I’m going to build a simple &lt;strong&gt;portfolio app&lt;/strong&gt; with a blog of articles.&lt;/p&gt;

&lt;p&gt;Before start building our project, let’s imagine two layers one to administrate the site and the other for public access (front pages). Here is how it should look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1060/1%2ArW0W8dEaRtJzuXRz3RCszA.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1060/1%2ArW0W8dEaRtJzuXRz3RCszA.jpeg" alt="Modules"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin (Dashboard):&lt;/strong&gt;&lt;br&gt;
Only admin users have access and permissions to post articles, add portfolio jobs, see reports, etc.&lt;br&gt;
There are many ways to build modules, for example using ActiveAdmin gem we can crate the admin module, another way is creating engines, but we are going to set up our dashboard module from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surface (front pages):&lt;/strong&gt;&lt;br&gt;
In this module, we’ll put all the public pages like home, about, portfolio, blog, and contact.&lt;br&gt;
Once we have clear this concept, let’s kick-off creating or project…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new portfolio &lt;span class="nt"&gt;-d&lt;/span&gt; postgresql &lt;span class="nt"&gt;-T&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;-T = skip testing modules. We’ll focus on test and design in other article&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Move under the Portfolio directory and run the following commands to create the database and migrations…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rails db:create
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Until here, we have a clean project with no custom pages or modules. So, let’s start creating our project architecture.&lt;br&gt;
First, under controllers create 2 folders called (admins, surface). Our tree would look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/controllers
  ├── admins
  └── surface
  └── application_controller.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Create these controllers under admins and surface folders &lt;code&gt;base_controller.rb&lt;/code&gt;, &lt;code&gt;blogs_controller.rb&lt;/code&gt;, &lt;code&gt;works_controller.rb&lt;/code&gt; and &lt;code&gt;pages_controller.rb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/controllers
  ├── admins # Portfolio blog and work management
  │   └── base_controller.rb
  |   └── blogs_controller.rb
  |   └── works_controller.rb
  └── surface # Public pages
  │   └── pages_controller.rb
  └── application.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let’s add some code to our admin controllers…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# admins/base_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admins::BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;# Define what users can access to this module&lt;/span&gt;
  &lt;span class="c1"&gt;# Define what view layout to use &lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;





&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#admins/works_controller.rb
# here we are extending from our Admins::BaseControler
class Admins::WorksController &amp;lt; Admins::BaseController
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;





&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# admins/blogs_controllers&lt;/span&gt;
&lt;span class="c1"&gt;# Inherits from Admins::BaseController&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admins::BlogsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Admins&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let’s add some code to our surface controller…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# surface/pages_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Surface::PagesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;#define our pages&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blog&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, we are going to add some routes to our app un &lt;code&gt;config/routes.rb&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Define our root page&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'surface/pages#home'&lt;/span&gt;

  &lt;span class="c1"&gt;# Frontend pages&lt;/span&gt;

  &lt;span class="c1"&gt;# scope module 'surface' to get this format url `domain.com/about, domain.com/work...` &lt;/span&gt;
  &lt;span class="c1"&gt;# and not `domain.com/surface/about...`&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;module: &lt;/span&gt;&lt;span class="s1"&gt;'surface'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_about'&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_work'&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_blog'&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_contact'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;These routes also can be isolated in files, we’ll isolate these routes in the next article.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, we should create our views according to our modules, let’s focus first in our Surface module, so let’s create the following folders and views.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/views
  ├── surface/pages # Write in each file &amp;lt;h1&amp;gt;Page Name&amp;lt;/h1&amp;gt; 
  │   └── home.html.erb     # I.e &amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;
  │   └── about.html.erb    # I.e &amp;lt;h1&amp;gt;About&amp;lt;/h1&amp;gt;
  |   └── work.html.erb     # I.e &amp;lt;h1&amp;gt;Work&amp;lt;/h1&amp;gt;
  |   └── blog.html.erb     # I.e &amp;lt;h1&amp;gt;Blog&amp;lt;/h1&amp;gt;
  |   └── contact.html.erb  # I.e &amp;lt;h1&amp;gt;Contact&amp;lt;/h1&amp;gt;
  └── partials   
  │   └── _header.html.erb
  |   └── _footer.html.erb
  └── layouts 
  |   └── application.html.erb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now let’s edit &lt;code&gt;application.html.erb&lt;/code&gt; under the layout folder and add the header and footer partials.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Partials are the components that will be used across the site.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s add some Html links to our &lt;code&gt;_header.html.erb&lt;/code&gt; under the partials folder...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Logo&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Home'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'About'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_about_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_work_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_blog_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_contact_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, add the header partial to our layout &lt;code&gt;applicaton.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"partials/header"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Alright! we have our surface module working perfectly 💯 if you go tho &lt;code&gt;http://localhost:3000&lt;/code&gt; you will be able to navigate between the pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NfZt6XAk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://miro.medium.com/max/600/1%2AXtKeK23_ESvngy7PnCmX4g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NfZt6XAk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://miro.medium.com/max/600/1%2AXtKeK23_ESvngy7PnCmX4g.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you got any error through this setup, feel free to drop me a line &lt;a href="mailto:dev.renzo.diaz@gmail.com"&gt;dev.renzo.diaz@gmail.com&lt;/a&gt; I’ll try to answer all your doubts asap.&lt;/p&gt;

&lt;p&gt;In the next &lt;a href="https://dev.to/renzodiaz/how-to-architect-rails-project-part-2-3be8"&gt;Part 2&lt;/a&gt;, will cover the admin module, authentication users, dashboard layout, CRUDS, and some of the front-end stuff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/renzodiaz"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PT2F9GG6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tutorial</category>
      <category>ruby</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
