Skip to Content
Course content

176: Practice Exercise: Building a Multi-Step Wizard Form Backend

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Where should I actually put the data while the user is moving between steps?

This is the first fork in the road. You have two real options: the session or a "draft" record in your database. For most onboarding flows—like a User Profile setup where you're collecting a username, then preferences, then a billing address—the session is your best bet. It's fast, and you don't clutter your database with half-finished accounts that people abandoned halfway through.

I usually wrap this in a helper method or a specialized session manager so the controller doesn't get messy. Here is how I'd structure the session storage for a multi-step sign-up:

# In your controller
def update_wizard_data
  # We store everything under a single key to keep the session organized
  session[:registration_data] ||= {}
  session[:registration_data].merge!(wizard_params)
end

private

def wizard_params
  # Only permit params relevant to the current step
  params.require(:user).permit(:username, :email, :theme, :notifications_enabled, :street, :city)
end

Just a heads up: if you're building something massive—think a 20-page mortgage application—stop using the session. Large hashes in the session can lead to cookie overflow errors or slow requests. In those cases, create a DraftApplication model and save to the DB at every "Next" click.

How do I stop someone from jumping straight to the last step by changing the URL?

You can't trust the user to follow your intended flow. If your final step is /wizard/step-3, a curious user will just type that into the address bar. If your code expects data from step 1 and 2 to be present, you'll likely hit a NoMethodError or save a bunch of null values.

I handle this with a simple guard method. I check if the required keys for the previous steps exist in the session. If they don't, I boot them back to the start. It's a bit blunt, but it works.

def ensure_step_completed!(step_number)
  required_keys = {
    2 => [:username, :email], # To get to step 2, step 1 must be done
    3 => [:theme, :notifications_enabled] # To get to step 3, step 2 must be done
  }

  keys = required_keys[step_number]
  return if keys.nil? || keys.all? { |k| session[:registration_data]&.key?(k) }

  redirect_to wizard_step_1_path, alert: "Please complete the previous steps first."
end

What's the cleanest way to save all this fragmented data into one record at the end?

Please, for the love of your future self, do not put the final persistence logic inside your controller action. By the time you reach the final "Submit" button, you're probably dealing with three different database tables (e.g., User, Profile, and BillingAddress). Putting that in a controller leads to a "Fat Controller" nightmare.

I always move this into a Service Object. The controller's only job should be to pass the session data to the service and handle the result. It keeps your logic testable and your controller lean.

# app/services/user_registration_service.rb
class UserRegistrationService
  def initialize(data)
    @data = data
  end

  def call
    ActiveRecord::Base.transaction do
      user = User.create!(
        username: @data[:username],
        email: @data[:email]
      )
      user.create_profile!(
        theme: @data[:theme],
        notifications: @data[:notifications_enabled]
      )
      user.create_billing_address!(
        street: @data[:street],
        city: @data[:city]
      )
      user
    end
  rescue ActiveRecord::RecordInvalid => e
    # Return the error or a custom failure object
    false
  end
end

In the controller, it becomes a one-liner: UserRegistrationService.new(session[:registration_data]).call. If it returns a user, you clear the session and redirect to the dashboard. Clean and simple.




📋 Practical Task

Exercise: Implementing the Step-Guard for a Subscription Wizard

You are building a 3-step subscription wizard: Step 1: Plan Selection (requires plan_id), Step 2: User Details (requires email), and Step 3: Payment Method (requires card_token).

Your task is to implement a verify_wizard_progress! method in the provided controller. The method must ensure that a user cannot access Step 2 without a plan_id in the session, and cannot access Step 3 without both a plan_id AND an email in the session.

class SubscriptionWizardController < ApplicationController
  before_action :verify_wizard_progress!, only: [:step_2, :step_3]

  def step_1; end
  def step_2; end
  def step_3; end

  private

  def verify_wizard_progress!
    # TODO: Implement the logic here
    # Use session[:subscription_data] to check for required keys
    # Redirect to step_1_path if requirements aren't met
  end
end
Rating
0 0

There are no comments for now.

to be the first to leave a comment.