Logo

dev-resources.site

for different kinds of informations.

Rails 8 CRUD: Modern Development Guide 2025

Published at
1/13/2025
Categories
rails
ruby
Author
sulmanweb
Categories
2 categories in total
rails
open
ruby
open
Author
9 person written this
sulmanweb
open
Rails 8 CRUD: Modern Development Guide 2025

When our team decided to upgrade to Rails 8, we chose a more Rails-native approach using importmap for JavaScript management. This decision aligned perfectly with Rails' philosophy of convention over configuration, and I'm excited to share how this choice shaped our development experience.

Initial Setup and Modern Stack Choices

Let's start with setting up our Rails 8 project:

rails new modern_platform \
  --css tailwind \
  --database postgresql \
  --skip-test \
  --skip-system-test
Enter fullscreen mode Exit fullscreen mode

Why no --javascript flag? Rails 8 comes with importmap by default, which I've found to be a game-changer for managing JavaScript dependencies. Here's how we configured our importmap:

# config/importmap.rb
pin "@hotwired/turbo-rails", to: "turbo.min.js"
pin "@hotwired/stimulus", to: "stimulus.min.js"
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js"

# Third-party packages we're using
pin "chart.js", to: "https://ga.jspm.io/npm:[email protected]/dist/chart.js"
pin "@rails/request.js", to: "https://ga.jspm.io/npm:@rails/[email protected]/src/index.js"
pin "trix"
pin "@rails/actiontext", to: "actiontext.js"

# Local JavaScript modules
pin_all_from "app/javascript/controllers", under: "controllers"
pin_all_from "app/javascript/components", under: "components"
Enter fullscreen mode Exit fullscreen mode

Modern JavaScript Organization

One of the benefits of importmap is how naturally it fits with module-based JavaScript. Here's how we structure our JavaScript:

// app/javascript/controllers/post_form_controller.js
import { Controller } from "@hotwired/stimulus"
import { post } from "@rails/request.js"

export default class extends Controller {
  static targets = ["form", "preview"]
  static values = {
    previewUrl: String
  }

  async preview() {
    const formData = new FormData(this.formTarget)

    try {
      const response = await post(this.previewUrlValue, {
        body: formData
      })

      if (response.ok) {
        const html = await response.text
        this.previewTarget.innerHTML = html
      }
    } catch (error) {
      console.error("Preview failed:", error)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Component-Based Architecture

We've embraced ViewComponent with Stimulus, creating a powerful combination for reusable UI components:

# app/components/rich_text_editor_component.rb
class RichTextEditorComponent < ViewComponent::Base
  attr_reader :form, :field

  def initialize(form:, field:)
    @form = form
    @field = field
  end

  def stimulus_controller_options
    {
      data: {
        controller: "rich-text-editor",
        rich_text_editor_toolbar_value: toolbar_options.to_json
      }
    }
  end

  private

  def toolbar_options
    {
      items: [
        %w[bold italic underline strike],
        %w[heading-1 heading-2],
        %w[link code],
        %w[unordered-list ordered-list]
      ]
    }
  end
end
Enter fullscreen mode Exit fullscreen mode
<!-- app/components/rich_text_editor_component.html.erb -->
<div class="rich-text-editor" <%= stimulus_controller_options %>>
  <%= form.rich_text_area field,
    class: "prose max-w-none",
    data: { 
      rich_text_editor_target: "editor",
      action: "trix-change->rich-text-editor#onChange"
    } %>

  <div class="mt-2 text-sm text-gray-500" 
       data-rich-text-editor-target="counter">
    0 characters
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
// app/javascript/controllers/rich_text_editor_controller.js
import { Controller } from "@hotwired/stimulus"
import Trix from "trix"

export default class extends Controller {
  static targets = ["editor", "counter"]
  static values = { 
    toolbar: Object,
    maxLength: Number 
  }

  connect() {
    this.setupToolbar()
    this.updateCounter()
  }

  onChange() {
    this.updateCounter()
  }

  updateCounter() {
    const text = this.editorTarget.value
    this.counterTarget.textContent = 
      `${text.length} characters`
  }

  setupToolbar() {
    if (!this.hasToolbarValue) return

    const toolbar = this.editorTarget
      .querySelector("trix-toolbar")

    // Customize toolbar based on configuration
    this.toolbarValue.items.forEach(group => {
      // Toolbar customization logic
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Chart.js Integration with Importmap

Here's how we handle data visualization using Chart.js through importmap:

// app/javascript/controllers/analytics_chart_controller.js
import { Controller } from "@hotwired/stimulus"
import { Chart } from "chart.js"

export default class extends Controller {
  static values = {
    data: Object,
    options: Object
  }

  connect() {
    this.initializeChart()
  }

  initializeChart() {
    const ctx = this.element.getContext("2d")

    new Chart(ctx, {
      type: "line",
      data: this.dataValue,
      options: {
        ...this.defaultOptions,
        ...this.optionsValue
      }
    })
  }

  get defaultOptions() {
    return {
      responsive: true,
      maintainAspectRatio: false,
      plugins: {
        legend: {
          position: "bottom"
        }
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Performance Optimizations with HTTP/2

One advantage of importmap is its excellent performance with HTTP/2. Here's how we optimize our asset delivery:

# config/environments/production.rb
Rails.application.configure do
  # Use CDN for importmapped JavaScript
  config.action_controller.asset_host = ENV["ASSET_HOST"]

  # Enable HTTP/2 Early Hints
  config.action_dispatch.early_hints = true

  # Configure importmap hosts
  config.importmap.cache_sweepers << Rails.root.join("app/javascript")

  # Preload critical JavaScript
  config.action_view.preload_links_header = true
end
Enter fullscreen mode Exit fullscreen mode

Testing JavaScript Components

We use Capybara with Cuprite for JavaScript testing:

# spec/system/posts_spec.rb
RSpec.describe "Posts", type: :system do
  before do
    driven_by(:cuprite)
  end

  it "previews post content", js: true do
    visit new_post_path

    find("[data-controller='post-form']").tap do |form|
      form.fill_in "Content", with: "**Bold text**"
      form.click_button "Preview"

      expect(form).to have_css(
        ".preview strong", 
        text: "Bold text"
      )
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Deployment Considerations

Our production setup leverages HTTP/2 and CDN caching:

# nginx.conf
server {
  listen 443 ssl http2;

  # Enable asset caching
  location /assets/ {
    expires max;
    add_header Cache-Control public;
  }

  # Early hints for importmapped JavaScript
  location / {
    proxy_pass http://backend;
    http2_push_preload on;
  }
}
Enter fullscreen mode Exit fullscreen mode

Parallel Query Execution: A Game-Changer

One of the most exciting features we discovered in Rails 8 was parallel query execution. During our performance optimization sprint, this became a crucial tool for handling complex dashboard pages:

# app/controllers/dashboards_controller.rb
class DashboardsController < ApplicationController
  def show
    # Execute multiple queries concurrently
    posts, comments, analytics, notifications = ActiveRecord::Future.all(
      fetch_recent_posts,
      fetch_pending_comments,
      fetch_analytics_data,
      fetch_user_notifications
    )

    respond_to do |format|
      format.html do
        render locals: {
          posts: posts,
          comments: comments,
          analytics: analytics,
          notifications: notifications
        }
      end

      format.turbo_stream do
        render turbo_stream: [
          turbo_stream.update("dashboard-posts", partial: "posts/list", locals: { posts: posts }),
          turbo_stream.update("dashboard-analytics", partial: "analytics/summary", locals: { data: analytics })
        ]
      end
    end
  end

  private

  def fetch_recent_posts
    Post.visible_to(current_user)
        .includes(:author, :categories)
        .order(published_at: :desc)
        .limit(10)
  end

  def fetch_pending_comments
    Comment.pending_review
           .includes(:post, :author)
           .where(post: { author_id: current_user.id })
           .limit(15)
  end

  def fetch_analytics_data
    AnalyticsService.fetch_dashboard_metrics(
      user: current_user,
      range: 30.days.ago..Time.current
    )
  end

  def fetch_user_notifications
    current_user.notifications
               .unread
               .includes(:notifiable)
               .limit(5)
  end
end
Enter fullscreen mode Exit fullscreen mode

To make this even more powerful, we integrated it with Stimulus for real-time updates:

// app/javascript/controllers/dashboard_controller.js
import { Controller } from "@hotwired/stimulus"
import { Chart } from "chart.js"

export default class extends Controller {
  static targets = ["analytics", "posts"]

  connect() {
    this.initializeCharts()
    this.startRefreshTimer()
  }

  disconnect() {
    if (this.refreshTimer) {
      clearInterval(this.refreshTimer)
    }
  }

  async refresh() {
    try {
      const response = await fetch(this.element.dataset.refreshUrl, {
        headers: {
          Accept: "text/vnd.turbo-stream.html"
        }
      })

      if (response.ok) {
        Turbo.renderStreamMessage(await response.text())
      }
    } catch (error) {
      console.error("Dashboard refresh failed:", error)
    }
  }

  startRefreshTimer() {
    this.refreshTimer = setInterval(() => {
      this.refresh()
    }, 30000) // Refresh every 30 seconds
  }

  initializeCharts() {
    if (!this.hasAnalyticsTarget) return

    const data = JSON.parse(this.analyticsTarget.dataset.metrics)
    this.createAnalyticsChart(data)
  }

  createAnalyticsChart(data) {
    const ctx = this.analyticsTarget.getContext("2d")

    new Chart(ctx, {
      type: "line",
      data: data,
      options: {
        responsive: true,
        maintainAspectRatio: false,
        animations: {
          tension: {
            duration: 1000,
            easing: 'linear'
          }
        }
      }
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

The combination of parallel queries and Turbo Streams gave us impressive performance improvements:

  1. Dashboard load times dropped by 47%
  2. Database connection usage became more efficient
  3. Real-time updates felt smoother with optimistic UI updates

Learning Journey and Trade-offs

Moving to importmap wasn't without challenges. Here's what we learned:

  1. Simplified Dependency Management: No more yarn/npm complexity
  2. Better Caching: HTTP/2 multiplexing improved load times
  3. Module Patterns: Encouraged cleaner JavaScript organization
  4. Development Experience: Faster feedback loop without build steps

Looking Forward

Rails 8 with importmap has transformed our development workflow. The native integration with Hotwire and Stimulus, combined with HTTP/2 optimizations, has given us a modern, maintainable, and performant application stack.

Stay tuned for more articles on our Rails 8 journey. Feel free to reach out with questions or share your own importmap experiences!


Happy Coding!


Originally published at https://sulmanweb.com

ruby Article's
30 articles in total
Favicon
Ruby on Rails 8 API not allowing mobile phone connection
Favicon
ruby -run
Favicon
ruby -run, again
Favicon
Ruby on Rails: Your Service Layer is a Lie
Favicon
Die Ruby-Seite von Puppet - Teil 2 - Benutzerdefinierte Funktionen
Favicon
The Ruby side of Puppet - Part 2 - Custom Functions
Favicon
Introducing Ephem
Favicon
What Is It (in Ruby 3.4)?
Favicon
Unable to find Ruby class that definitely exists
Favicon
Devise not accepting JSON Token
Favicon
Ruby on Rails - Calculating pricing based user's purchasing power parity
Favicon
Ruby on Rails 8 - Frontend Rรกpido Usando Tailwind como um Frameworks CSS Classless
Favicon
Use cases for Turbo's Custom Events
Favicon
Meta programming with Ruby Eval: A guide (Part 1)
Favicon
Tracing a method call in Ruby
Favicon
False positives of Lint/Void in assignments
Favicon
Die Ruby-Seite von Puppet - Teil 1 - Benutzerdefinierte Fakten
Favicon
Just committed to learning ruby for sonic pi and rails https://dev.to/highcenburg/2025-roadmap-mastering-ruby-for-sonic-pi-and-rails-696 wish me luck!
Favicon
Building a GitHub Activity CLI - A Ruby Journey
Favicon
I have been thinking of moving forward from Python to Ruby to align my skills with my musicality since I like to learn SonicPi.. But I'm still thinking about it so yeah
Favicon
Task Tracker CLI
Favicon
Rails 8 CRUD: Modern Development Guide 2025
Favicon
When Controllers Take on Too Much Responsibility
Favicon
Ruby on Rails 8 - Frontend Rรกpido com Frameworks CSS Classless ou Class-Light sem CDN
Favicon
Cdg Hoodie or Eric Emanuel Hoodie: The Trend You Need Now
Favicon
Brakeman LSP Support
Favicon
Docker in development: Episode 3
Favicon
Add Invite to Rails 8 Authentication
Favicon
How to Develop an Air Quality Monitoring App with Ruby on Rails
Favicon
School

Featured ones: