Advanced Rails Techniques: Beyond the Basics


You master the basics of Rails ? It's time to level up! Let's explore together advanced techniques that will make your code more expressive, maintainable and... "railsy". In this article, we will explore how to use class methods, organize your code with concerns, create your own generators and even extend Rails with custom features.


So what are we waiting for? Let's go :)


The article was written following Chris Oliver's presentation at Rails World 2025, you can find the original video at the end.

What makes the code "railsy" ?


Rails has this unique ability to render code readable as if you were reading a book. Think of methods like has_many :subscribers or has_one_attached :featured_image. It's magic, right? This expressiveness comes from Ruby and the Rails philosophy that your code should stand on an equal footing with the framework itself.


In controllers, we find this same elegance with before_action :authenticate_user. Rails 8 pushes this concept even further with methods like :


allow_unauthenticated_access
rate_limit to: 10, within: 5.minutes


These methods visually stand out from the traditional before_action, and that's deliberate !

Create your own class methods Rails

Beyond the before_action traditional

You could write this in your controllers :

class AdminController < ApplicationController
before_action :require_admin
private
def require_admin
# logique d'autorisation
end
end


But why not do like Rails and create a more expressive class method ?


class AdminController < ApplicationController
admin_access_only
end

It's still clearer, right ?

(And on top of that, it’s easier on the eyes than hunting the right before_action in a list of ten !)

Implementing custom class methods

Here's how to create this magic in an authorization concern :


module Authorization
extend ActiveSupport::Concern
class_methods do
def admin_access_only(message: "Access denied", **options)
before_action(options) do
redirect_to root_path, alert: message unless current_user&.admin?
end
end
end
end


The advantage of class methods ? You can pass arguments! No more creating ten methods for ten use cases.

Organize your code with custom folders

Going off the beaten path

Rails doesn't force you to stuff everything into models, views and controllers. You can create your own folders in app/ :


  1. app/api_clients/ for your API clients
  2. app/ui_components/ for your UI components
  3. app/validators/ for your custom validations
  4. app/notifiers/ for your notifications
  5. app/services/ for your services


Rails will automatically load these folders. It's like having a drawer for every type of utensil in your kitchen! This will make your code so much nicer to read and maintain.

Concrete example: custom API clients

Instead of using an external gem that implements 200 endpoints of which you only need one, create your own client :


# app/api_clients/github_client.rb
class GithubClient
BASE_URI = "https://api.github.com"
def invite_to_organization(username, organization)
# Une seule méthode, une seule responsabilité
end
end


Simpler, more maintainable, and zero external dependencies!

Custom Rails generators

Creating your own tools

Rails lets you create your own generators. It's perfect for standardizing patterns in your team :


rails generate generator api_client


This creates a generator that inherits from NamedBase, automatically handling arguments and file paths :


class ApiClientGenerator < Rails::Generators::NamedBase
source_root File.expand_path("templates", __dir__)
class_option :url, type: :string, desc: "Base URL for the API"
def create_api_client
template "api_client.rb.erb", "app/api_clients/#{file_name}_client.rb"
end
end

Customize existing templates

Are you still using scaffolds to make your life easier? But you’re tired of <%= notice %> when you already have it in your layout ?


Well, easy Monique. Override the template :


<!-- lib/templates/erb/scaffold/show.html.erb.tt -->
<!-- Supprimez juste la ligne du notice -->


Now your scaffolds adapt to your application !

Concerns for organization, not just reuse

Organize by feature

Concerns aren't just for sharing code. You can, and some would say you should, use them to organize models by feature :


class User < ApplicationRecord
include User::Billing
include User::Mentions
end


These modules live in app/models/user/ and contain all the logic related to each feature. No more 500-line models where everything gets mixed!


# app/models/user/billing.rb
module User::Billing
extend ActiveSupport::Concern
included do
has_many :subscriptions
has_many :invoices
end
def billing_active?
# toute la logique de facturation ici
end
end


A complete example: Cloudflare Turnstile integration

What is Cloudfare Turnstile

Cloudflare Turnstile is a modern alternative to traditional CAPTCHAs (you know, those annoying tests where you have to identify traffic lights or crosswalks 😅).


The problems it solves:

  1. Bots : Automated programs can spam your forms, create fake accounts, etc.
  2. Traditional CAPTCHAs : Frustrating for users, sometimes hard to solve, not always accessible


Turnstile verifies that the user is human in an almost invisible way. It analyzes the browser's behavior in the background, without bothering the user with puzzles. It's free and more privacy-friendly than reCAPTCHA.

The initial problem

You want to integrate Turnstile into your signup form. First instinct :

def create
if verify_turnstile(params[:turnstile_token])
@user = User.new(user_params)
if @user.save
redirect_to root_path
else
render :new
end
else
flash[:error] = "Turnstile verification failed"
render :new
end
end


Why is that frustrating ?

Imagine this scenario :

  1. A user fills out your signup form
  2. They forget to fill in their email (validation error)
  3. They submit the form
  4. Turnstile passes (it's human), but the email is missing
  5. The form reloads with the error "Email required"
  6. They add their email and re-submit
  7. New Turnstile challenge ! 😤

The Turnstile token is single-use. Each submission = a new challenge.

The elegant solution

Create a reusable concern :

# app/models/concerns/challengeable.rb
module Challengeable
extend ActiveSupport::Concern
included do
attr_accessor :challenge_token
validates :challenge_token, turnstile: true
end
end
  1. attr_accessor :challenge_token : Creates a temporary attribute (not in the database) to store the Turnstile token
  2. validates :challenge_token, turnstile: true : Adds a validation that checks the token with Cloudflare

Why a concern ? You can easily add it to any model (User, Comment, ContactForm, etc.)


Now create a custom validator

# app/validators/turnstile_validator.rb
class TurnstileValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
unless TurnstileClient.new.verify(value, record.current_ip)
record.errors.add(attribute, "Verification failed")
end
end
end


What happens :

  1. Rails automatically calls this validator when you do @user.save
  2. It checks the token with the Cloudflare API
  3. If it fails, it adds an error like any other validation


Now in your model :

class User < ApplicationRecord
include Challengeable
end

One line, and your User has Turnstile protection now.


And your controller becomes simple again :


def create
@user = User.new(user_params)
if @user.save
redirect_to root_path
else
render :new # Affiche TOUTES les erreurs, y compris Turnstile
end
end


## Why is this great?
### Before (naive approach):
User submits → Turnstile verified ✅ → Email missing ❌
Form reloaded → User corrects email → Submits
NEW Turnstile required ❌ (frustration ++)
## After (elegant approach):
User submits → Validation in one block
Errors displayed: "Email required" + "Turnstile invalid"
User corrects EVERYTHING → Submits once ✅

Extending Rails with template handlers

The idea

Rails uses a system of template handlers to process different formats. index.html.erb ? The ERB handler processes the file. index.json.jbuilder ? It's the JBuilder handler that handles it.


You can create your own handlers ! (Well, the PHP example in the talk was... let's say... "original", but the principle remains valid.)

Practical example: Farem PDF

Here's a more serious example with the gem Farem PDF that generates PDFs without Node.js :


# Dans votre contrôleur
def invoice
respond_to do |format|
format.html
format.pdf { render farem_pdf: { landscape: true } }
end
end


The custom renderer :

  1. Captures the rendered HTML
  2. Launches Chrome in headless mode
  3. Generates the PDF
  4. Returns it to the browser


All of this in a transparent way !

Conclusion


These advanced techniques transform Rails from a web framework into a true domain language. By using the class methods, organizing by concerns, custom generators, and Rails extensions, you create a code that tells a story and reads like a book.


The secret ? Don't be afraid to extend Rails as needed. The framework is designed to be customized, so take advantage of it! And remember : if your code doesn't read like English, there's probably a more "railsy" way to write it.



The original presentation by Chris Oliver:









Share it: