Ruby on Rails
03 / 04

Views, Mailers & Jobs

Views, Mailers & Background Jobs

Rails views use ERB templates with layouts and partials. ActionMailer handles email delivery. ActiveJob provides a unified background job interface with adapters for Sidekiq, Delayed::Job, and others.

ERB Templates & Helpers

<!-- app/views/articles/index.html.erb -->
<% content_for :title, "Articles" %>

<h1>Articles</h1>

<% if @articles.any? %>
  <div class="articles">
    <% @articles.each do |article| %>
      <%# Render a partial (app/views/articles/_article.html.erb) %>
      <%= render article %>
      <%# Equivalent: render partial: "article", locals: { article: article } %>
    <% end %>
  </div>

  <!-- Pagination links (kaminari gem) -->
  <%= paginate @articles %>
<% else %>
  <p>No articles yet. <%= link_to "Create one", new_article_path %>.</p>
<% end %>

<!-- app/views/articles/_article.html.erb -->
<article class="article-card" id="article-<%= article.id %>">
  <h2><%= link_to article.title, article_path(article) %></h2>
  <p class="meta">
    By <%= article.user.name %> &middot;
    <%= time_tag article.created_at, article.created_at.strftime("%B %d, %Y") %>
  </p>
  <p><%= truncate(article.body, length: 200) %></p>
</article>

<!-- form_with generates a form that works with Rails UJS/Turbo -->
<!-- app/views/articles/_form.html.erb -->
<%= form_with model: @article do |f| %>
  <% if @article.errors.any? %>
    <div class="errors">
      <ul>
        <% @article.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  <% end %>

  <%= f.label :title %>
  <%= f.text_field :title, class: "form-control" %>

  <%= f.label :body %>
  <%= f.text_area :body, rows: 10, class: "form-control" %>

  <%= f.check_box :published %> <%= f.label :published %>

  <%= f.submit class: "btn btn-primary" %>
<% end %>

ActionMailer

# Generate a mailer
# rails generate mailer User welcome password_reset

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  default from: "DevRecall <noreply@devrecall.com>"

  def welcome
    @user = params[:user]
    @login_url = login_url

    mail(
      to: @user.email,
      subject: "Welcome to DevRecall!"
    )
    # Renders app/views/user_mailer/welcome.html.erb + welcome.text.erb
  end

  def password_reset
    @user = params[:user]
    @token = params[:token]
    @expires_at = 2.hours.from_now

    mail(
      to: @user.email,
      subject: "Reset your password"
    )
  end
end

# app/views/user_mailer/welcome.html.erb
# <h1>Welcome, <%= @user.first_name %>!</h1>
# <p>Click here to get started: <%= link_to "Go to Dashboard", @login_url %></p>

# Deliver methods
UserMailer.with(user: @user).welcome.deliver_now     # Synchronous (blocks request)
UserMailer.with(user: @user).welcome.deliver_later   # Async via ActiveJob (preferred)

# config/environments/development.rb — use Letter Opener to preview emails in browser
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true

# config/environments/production.rb
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address: "smtp.sendgrid.net",
  port: 587,
  user_name: "apikey",
  password: Rails.application.credentials.sendgrid_api_key,
  authentication: :plain,
  enable_starttls_auto: true
}

ActiveJob & Rails Credentials

# Generate a job
# rails generate job ProcessPayment

# app/jobs/process_payment_job.rb
class ProcessPaymentJob < ApplicationJob
  queue_as :default                # Queue name
  retry_on Stripe::RateLimitError, wait: 5.seconds, attempts: 3
  discard_on ActiveRecord::RecordNotFound

  def perform(order_id)
    order = Order.find(order_id)      # raises RecordNotFound → discarded
    PaymentService.new(order).process!
    OrderMailer.with(order: order).confirmation.deliver_later
  end
end

# Enqueue a job
ProcessPaymentJob.perform_later(order.id)
ProcessPaymentJob.set(wait: 5.minutes).perform_later(order.id)
ProcessPaymentJob.set(wait_until: Date.tomorrow.noon).perform_later(order.id)
ProcessPaymentJob.perform_now(order.id)   # Synchronous (for testing)

# Queue adapters (config/application.rb or environments)
config.active_job.queue_adapter = :sidekiq  # Production
config.active_job.queue_adapter = :async    # Development (in-process, no persistence)
config.active_job.queue_adapter = :test     # Test (enqueues but does not run)

# Sidekiq setup (Gemfile: gem "sidekiq")
# config/initializers/sidekiq.rb
Sidekiq.configure_server { |c| c.redis = { url: ENV["REDIS_URL"] } }
Sidekiq.configure_client { |c| c.redis = { url: ENV["REDIS_URL"] } }
# config/application.rb: config.active_job.queue_adapter = :sidekiq
# Start: bundle exec sidekiq

# Rails Credentials — encrypted secrets, committed to git
# EDITOR="code --wait" rails credentials:edit   # Open in VS Code
# Structure (config/credentials.yml.enc):
# secret_key_base: abc123...
# sendgrid_api_key: SG.xxx
# stripe:
#   secret_key: sk_live_xxx
#   webhook_secret: whsec_xxx

# Access in code
Rails.application.credentials.sendgrid_api_key
Rails.application.credentials.stripe[:secret_key]
# Environment-specific credentials
# rails credentials:edit --environment production
Rails.application.credentials.stripe[:secret_key]  # reads from config/credentials/production.yml.enc

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free