| name | rails |
| description | Enterprise Ruby on Rails development with Active Record, API mode, testing, and production patterns |
| category | frameworks |
| triggers | ["rails","ruby on rails","ror","active record","ruby api","rails api","ruby web","rubygems"] |
Ruby on Rails
Enterprise-grade Ruby on Rails development following industry best practices. This skill covers Active Record, API mode, service objects, authentication, testing with RSpec, background jobs, and production deployment configurations used by top engineering teams.
Purpose
Build scalable Ruby applications with confidence:
- Design clean model architectures with Active Record
- Implement REST APIs with Rails API mode
- Use service objects and concerns for clean code
- Handle authentication with JWT or Devise
- Write comprehensive tests with RSpec
- Deploy production-ready applications
- Leverage background jobs with Sidekiq
Features
1. Model Design and Associations
class User < ApplicationRecord
has_secure_password
has_many :memberships, dependent: :destroy
has_many :organizations, through: :memberships
has_many :owned_organizations, class_name: 'Organization', foreign_key: :owner_id
has_many :projects, foreign_key: :created_by
validates :email, presence: true, uniqueness: { case_sensitive: false },
format: { with: URI::MailTo::EMAIL_REGEXP }
validates :name, presence: true, length: { minimum: 2, maximum: 100 }
validates :password, length: { minimum: 8 }, if: -> { new_record? || password.present? }
validates :role, inclusion: { in: %w[admin user guest] }
before_save :downcase_email
scope :active, -> { where(is_active: true) }
scope :admins, -> { where(role: 'admin') }
scope :search, ->(query) {
return all if query.blank?
where('name ILIKE :q OR email ILIKE :q', q: "%#{query}%")
}
enum :role, { guest: 'guest', user: 'user', admin: 'admin' }, default: :user
def admin?
role == 'admin'
end
def member_of?(organization)
organizations.exists?(organization.id)
end
private
def downcase_email
self.email = email.downcase
end
end
class Organization < ApplicationRecord
belongs_to :owner, class_name: 'User'
has_many :memberships, dependent: :destroy
has_many :members, through: :memberships, source: :user
has_many :projects, dependent: :destroy
validates :name, presence: true, length: { maximum: 255 }
validates :slug, presence: true, uniqueness: true,
format: { with: /\A[a-z0-9-]+\z/ }
scope :for_user, ->(user) { joins(:memberships).where(memberships: { user_id: user.id }) }
before_validation :generate_slug, on: :create
private
def generate_slug
self.slug ||= name&.parameterize
end
end
class Project < ApplicationRecord
belongs_to :organization
belongs_to :creator, class_name: 'User', foreign_key: :created_by
has_many :tasks, dependent: :destroy
validates :name, presence: true, length: { maximum: 255 }
validates :name, uniqueness: { scope: :organization_id }
enum :status, { draft: 'draft', active: 'active', completed: 'completed', archived: 'archived' }
scope :active, -> { where(status: :active) }
include Discard::Model
default_scope -> { kept }
end
2. Serializers
class UserSerializer
include JSONAPI::Serializer
attributes :id, :email, :name, :role, :is_active, :created_at, :updated_at
attribute :organization_count do |user|
user.organizations.count
end
has_many :organizations, serializer: OrganizationSerializer, if: Proc.new { |_record, params|
params && params[:include_organizations]
}
end
class OrganizationSerializer
include JSONAPI::Serializer
attributes :id, :name, :slug, :created_at
attribute :member_count do |organization|
organization.members.count
end
belongs_to :owner, serializer: UserSerializer
end
class PaginationSerializer
def initialize()
= collection
= serializer_class
= options
{
serialized_data,
{
.current_page,
.limit_value,
.total_count,
.total_pages,
.current_page < .total_pages
}
}
.new(, ).serializable_hash[]
3. Controllers
class ApplicationController < ActionController::API
include ActionController::HttpAuthentication::Token::ControllerMethods
before_action :authenticate_user!
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from ActiveRecord::RecordInvalid, with: :unprocessable_entity
private
def authenticate_user!
authenticate_or_request_with_http_token do |token, _options|
@current_user = JsonWebToken.decode(token)
end
end
def current_user
@current_user
end
def authorize_admin!
render_forbidden unless current_user.admin?
end
def not_found(exception)
render json: { error: { code: 'NOT_FOUND', message: exception.message } }, status: :not_found
end
def unprocessable_entity(exception)
render { { , exception.record.errors } },
render { { , } },
<
before_action , [, ]
before_action , [, , ]
users = .active.search(params[])
users = users.where( params[]) params[].present?
users = users.page(params[]).per(params[] || )
render .new(users, ).as_json
render .new(, ).serializable_hash
user = .new(user_params)
user.save!
render .new(user).serializable_hash,
.update!(user_params)
render .new().serializable_hash
.destroy
head
render .new(current_user, ).serializable_hash
= .find(params[])
params.().permit(, , , , )
<
skip_before_action , [, ]
user = .new(register_params)
user.save!
token = .encode( user.id)
render { .new(user).serializable_hash, token },
user = .find_by( params[]&.downcase)
user&.authenticate(params[]) && user.is_active?
token = .encode( user.id)
render { .new(user).serializable_hash, token }
render { { , } },
params.().permit(, , )
4. Service Objects
class ApplicationService
def self.call(...)
new(...).call
end
end
module Users
class CreateUserService < ApplicationService
def initialize(params)
@params = params
end
def call
user = User.new(@params)
user.save!
SendWelcomeEmailJob.perform_later(user.id)
Result.success(user: user)
rescue ActiveRecord::RecordInvalid => e
Result.failure(errors: e.record.errors)
end
end
end
module Organizations
class CreateOrganizationService < ApplicationService
def initialize(owner:, params:)
@owner = owner
@params = params
end
.transaction
organization = .create!(.merge( ))
.create!( , organization, )
.success( organization)
=> e
.failure( e.record.errors)
,
()
= success
= data
= errors
!
.success(data = {})
new( , data)
.failure()
new( , errors)
5. JWT Authentication
class JsonWebToken
SECRET_KEY = Rails.application.credentials.secret_key_base
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET_KEY)
end
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY)[0]
User.find(decoded['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
nil
end
end
6. Background Jobs
class ApplicationJob < ActiveJob::Base
queue_as :default
retry_on StandardError, wait: :exponentially_longer, attempts: 3
discard_on ActiveJob::DeserializationError
end
class SendWelcomeEmailJob < ApplicationJob
queue_as :mailers
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome_email(user).deliver_now
end
end
7. Testing with RSpec
require 'rails_helper'
RSpec.describe User, type: :model do
describe 'validations' do
subject { build(:user) }
it { should validate_presence_of(:email) }
it { should validate_uniqueness_of(:email).case_insensitive }
it { should validate_presence_of(:name) }
end
describe 'associations' do
it { should have_many(:memberships).dependent(:destroy) }
it { should have_many(:organizations).through(:memberships) }
end
describe 'scopes' do
describe '.active' do
it 'returns only active users' do
active_user = create(:user, is_active: true)
inactive_user = create(:user, is_active: false)
expect(User.active).to include(active_user)
expect(User.active).not_to include(inactive_user)
end
end
describe '.search' do
it 'searches by name and email' do
user = create(:user, name: 'John Doe', )
expect(.search()).to (user)
expect(.search()).not_to (user)
describe
it
admin = build(, )
expect(admin.admin?).to be
.describe ,
let() { create(, ) }
let() { { => } }
describe
before { create_list(, ) }
it
get , auth_headers
expect(response).to have_http_status()
expect(json_response[]).to be_an()
expect(json_response[]).to (, )
it
user = create()
headers = { => }
get , headers
expect(response).to have_http_status()
describe
let() { { { , , } } }
it
expect {
post , valid_params, auth_headers
}.to change(, ).by()
expect(response).to have_http_status()
.parse(response.body)
.define
factory
sequence() { || }
name { .name }
password { }
role { }
is_active { }
trait
role { }
Use Cases
API Rate Limiting
class Rack::Attack
throttle('requests by ip', limit: 100, period: 1.minute) do |request|
request.ip
end
throttle('login attempts', limit: 5, period: 1.minute) do |request|
if request.path == '/api/v1/auth/login' && request.post?
request.ip
end
end
end
Caching with Redis
def show
@organization = Rails.cache.fetch("organization:#{params[:id]}", expires_in: 5.minutes) do
Organization.includes(:owner, :members).find(params[:id])
end
render json: OrganizationSerializer.new(@organization).serializable_hash
end
Best Practices
Do's
- Use UUID primary keys for public APIs
- Use service objects for business logic
- Use serializers for consistent responses
- Use concerns for shared behavior
- Use scopes for reusable queries
- Use background jobs for heavy operations
- Use strong parameters
- Write comprehensive tests with RSpec
- Use database indexes for performance
- Use soft deletes for important data
Don'ts
- Don't put business logic in controllers
- Don't use N+1 queries
- Don't skip validations
- Don't ignore security headers
- Don't expose internal errors
- Don't use callbacks for business logic
- Don't skip authentication
- Don't ignore test coverage
- Don't use sync operations for heavy tasks
- Don't forget rate limiting
References