This is the first article in a completely new section on Rails Designer: Build a SaaS with Ruby on Rails. It will be a collection of pragmatic, practical articles with tips, insights and best practices on building a SaaS with Rails in 2024 and beyond.
Rails 8 ships (finally!) with authentication out-of-the-box. ❤️ When it launched most were surprised it didn’t come with a way to register (sign up) a new user. But this was a conscious decision, because creating a User isn’t typically the only resource needed for a SaaS. There are many other actions that need to be taken and resources created when someone new signs up for your product.
This article explores the way I do it using something, typically referred to as a Form Object. Let’s check it out.
If you want to follow along, check out this repo. This given commit is a vanilla Rails 8 app where bin/rails generate authentication has been run.
For new features, I always like to start from the outside in. This means I start with the UI and then work my way into the internals.
Let’s create the form at app/views/signups/new.html.erb:
<%= tag.div(flash[:alert], style: "color:red") if flash[:alert] %>
<%= tag.div(flash[:notice], style: "color:green") if flash[:notice] %>
<%= form_with model: @signup do |form| %>
<%= form.email_field :email_address, required: true, autofocus: true, autocomplete: "username", placeholder: "Enter your email address", value: params[:email_address] %><br>
<%= form.password_field :password, required: true, autocomplete: "current-password", placeholder: "Enter your password", maxlength: 72 %><br>
<%= form.submit "Sign up" %>
<% end %>
It’s pretty similar to Rails’ sessions/new.html.erb.
Note: articles in this section focus on the (business) logic and come without any styling. I suggest to check out the vast collection of articles on Rails Designer to improve your UI, CSS and JavaScript skills! 🧑🎓 🎨
Next up the route:
# config/routes.rb
root to: "pages#show"
resource :signups, path: "signup", only: %w[new create]
I also already added a root route, where the user will be redirected to upon successful sign up.
Then the controller. It is pretty straight-forward:
class SignupsController < ApplicationController
allow_unauthenticated_access only: %w[new create]
def new
@signup = Signup.new
end
def create
@signup = Signup.new(signup_params)
if user = @signup.save
start_new_session_for user
redirect_to root_url
else
redirect_to new_signups_path
end
end
private
def signup_params
params.expect(signup: [ :email_address, :password ])
end
end
allow_unauthenticated_access is coming from Rails’ authentication generator. Then another little new thing is params.expect(signup: [ :email_address, :password, :terms ]). Previously you might have seen params.require(:signup).permit(:email_address, :password). It’s a new Rails 8+ syntax, added in this PR.
It all looks pretty familiar, right? But the special bit is in the Signup class. This won’t be your regular ActiveModel, but will be a so called Form Object (just a typical Plain Old Ruby Object).
# app/models/signup.rb
class Signup
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Attributes::Normalization # Available from Rails 8.1+
attribute :email_address, :string
attribute :password, :string
normalizes :email_address, with: -> { it.strip.downcase }
validates :email_address, presence: true
validates :password, length: 8..128
def save
if valid?
User.create!(email_address: email_address, password: password)
end
end
def model_name
ActiveModel::Name.new(self, nil, self.class.name)
end
end
You see I just store it in app/models; no other folder needed. Also note it’s just a vanilla Ruby class (often referred to as PORO), ie. it doesn’t inherit from ApplicationRecord, but it looks (and quacks!) pretty much like one.
That is due to the model_name method and the ActiveModel::Model and ActiveModel::Attributes modules included. This allows you to reference the object in the form builder (form_with model: Signup.new).
To me the beauty of this set up is you now have a class that can be responsible for much more. After all, when signing up for a new SaaS often many more things happen:
- create a workspace
- create a team
- send welcome email
- create sign up event
- and so on…
Whatever else is needed for your specific use case. Let’s extend above class a bit with an example:
# app/models/signup.rb
class Signup
# …
def save
if valid?
+ User.create!(email_address: email_address, password: password).tap do |user|
+ create_workspace_for user
+ send_welcome_email_to user
+ end
end
end
def model_name
ActiveModel::Name.new(self, nil, self.class.name)
end
private
+ def create_workspace_for(user)
+ # eg. user.workspaces.create
+ end
+ def send_welcome_email_to(user)
+ # eg. `WelcomeEmail.perform_later user`
+ # or using the Courrier gem: `WelcomeEmail.deliver to: user.email_address`,
+ # see https://github.com/Rails-Designer/courrier/ for more information
+ end
end
Now when valid? returns true, a workspace is created and a welcome email is sent. It also uses tap: this will pass along the user object and returns it (which is useful in the SignupsController#create action).
I like to write my methods to be easy to read and understand (similar to how the Rails’ 8 authentication code is written, e.g. start_new_session_for user).
I have now only added two extra steps, but you can imagine that Workspace creation would happen in another class again too. Feel free to extend with whatever you need.
Let’s not stop here, let’s add a checkbox for the user to accept your Terms of Service. First update the view:
<%= form_with model: @signup do |form| %>
<%# … %>
<%= form.check_box :terms %>
<%= form.label :terms %><br>
<% end %>
Then validate it is present in the Signup class.
class Signup
# …
attribute :terms, :boolean, default: false
validates :terms, acceptance: true
# …
end
Then allow the attribute to be passed along in the params.
class SignupsController < ApplicationController
# …
def signup_params
params.expect(signup: [ :email_address, :password, :terms ])
end
end
Similarly you can add a checkbox to opt them in for your email newsletter (and store as a preference using Rails Vault). 💡
To finalize this from start to finish, let’s create a simple view where the user is redirected to upon sign up.
# app/views/pages/show.html.erb
<p>Sign up Successful</p>
<%= button_to "Log out", session_path, method: :delete %>
Using this approach you have a class that can contain all required steps for sign ups. Easy to reason about, and easy to test!
And with that you have added sign ups to your Rails 8 app. Also this code is not limited to Rails 8, and can be as easily added to older versions of Rails.
💬 Over to you…
What did you like about this article? Learned something knew? Found something is missing or even broken? 🫣 Let me (and others) know!
Comments are powered by Chirp Form
{{comment}}