Enterprise Ruby on Rails development with Active Record, API mode, testing, and production patterns
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.
Build scalable Ruby applications with confidence:
# app/models/user.rb
class User < ApplicationRecord
has_secure_password
# Associations
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
# Validations
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] }
# Callbacks
before_save :downcase_email
# Scopes
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}%")
}
# Enums
enum :role, { guest: 'guest', user: 'user', admin: 'admin' }, default: :user
# Instance methods
def admin?
role == 'admin'
end
def member_of?(organization)
organizations.exists?(organization.id)
end
private
def downcase_email
self.email = email.downcase
end
end
# app/models/organization.rb
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
# app/models/project.rb
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
# app/serializers/user_serializer.rb
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
# app/serializers/organization_serializer.rb
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
# app/serializers/pagination_serializer.rb
class PaginationSerializer
def initialize(collection, serializer_class, options = {})
@collection = collection
@serializer_class = serializer_class
@options = options
end
def as_json
{
data: serialized_data,
pagination: {
current_page: @collection.current_page,
per_page: @collection.limit_value,
total: @collection.total_count,
total_pages: @collection.total_pages,
has_more: @collection.current_page < @collection.total_pages
}
}
end
private
def serialized_data
@serializer_class.new(@collection, @options).serializable_hash[:data]
end
end
# app/controllers/application_controller.rb
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 json: { error: { code: 'VALIDATION_ERROR', details: exception.record.errors } }, status: :unprocessable_entity
end
def render_forbidden
render json: { error: { code: 'FORBIDDEN', message: 'Access denied' } }, status: :forbidden
end
end
# app/controllers/api/v1/users_controller.rb
module Api
module V1
class UsersController < ApplicationController
before_action :authorize_admin!, except: [:me, :update_me]
before_action :set_user, only: [:show, :update, :destroy]
def index
users = User.active.search(params[:search])
users = users.where(role: params[:role]) if params[:role].present?
users = users.page(params[:page]).per(params[:per_page] || 20)
render json: PaginationSerializer.new(users, UserSerializer).as_json
end
def show
render json: UserSerializer.new(@user, include_organizations: true).serializable_hash
end
def create
user = User.new(user_params)
user.save!
render json: UserSerializer.new(user).serializable_hash, status: :created
end
def update
@user.update!(user_params)
render json: UserSerializer.new(@user).serializable_hash
end
def destroy
@user.destroy
head :no_content
end
def me
render json: UserSerializer.new(current_user, include_organizations: true).serializable_hash
end
private
def set_user
@user = User.find(params[:id])
end
def user_params
params.require(:user).permit(:name, :email, :password, :role, :is_active)
end
end
end
end
# app/controllers/api/v1/auth_controller.rb
module Api
module V1
class AuthController < ApplicationController
skip_before_action :authenticate_user!, only: [:register, :login]
def register
user = User.new(register_params)
user.save!
token = JsonWebToken.encode(user_id: user.id)
render json: { user: UserSerializer.new(user).serializable_hash, token: token }, status: :created
end
def login
user = User.find_by(email: params[:email]&.downcase)
if user&.authenticate(params[:password]) && user.is_active?
token = JsonWebToken.encode(user_id: user.id)
render json: { user: UserSerializer.new(user).serializable_hash, token: token }
else
render json: { error: { code: 'UNAUTHORIZED', message: 'Invalid credentials' } }, status: :unauthorized
end
end
private
def register_params
params.require(:user).permit(:name, :email, :password)
end
end
end
end
# app/services/application_service.rb
class ApplicationService
def self.call(...)
new(...).call
end
end
# app/services/users/create_user_service.rb
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
# app/services/organizations/create_organization_service.rb
module Organizations
class CreateOrganizationService < ApplicationService
def initialize(owner:, params:)
@owner = owner
@params = params
end
def call
ActiveRecord::Base.transaction do
organization = Organization.create!(@params.merge(owner: @owner))
Membership.create!(user: @owner, organization: organization, role: :owner)
Result.success(organization: organization)
end
rescue ActiveRecord::RecordInvalid => e
Result.failure(errors: e.record.errors)
end
end
end
# app/services/result.rb
class Result
attr_reader :data, :errors
def initialize(success:, data: {}, errors: nil)
@success = success
@data = data
@errors = errors
end
def success?
@success
end
def failure?
!@success
end
def self.success(data = {})
new(success: true, data: data)
end
def self.failure(errors:)
new(success: false, errors: errors)
end
end
# lib/json_web_token.rb
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
# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
queue_as :default
retry_on StandardError, wait: :exponentially_longer, attempts: 3
discard_on ActiveJob::DeserializationError
end
# app/jobs/send_welcome_email_job.rb
class SendWelcomeEmailJob < ApplicationJob
queue_as :mailers
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome_email(user).deliver_now
end
end
# spec/models/user_spec.rb
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', email: 'john@example.com')
expect(User.search('john')).to include(user)
expect(User.search('jane')).not_to include(user)
end
end
end
describe '#admin?' do
it 'returns true for admin users' do
admin = build(:user, role: :admin)
expect(admin.admin?).to be true
end
end
end
# spec/requests/api/v1/users_spec.rb
require 'rails_helper'
RSpec.describe 'Api::V1::Users', type: :request do
let(:admin) { create(:user, role: :admin) }
let(:auth_headers) { { 'Authorization' => "Bearer #{JsonWebToken.encode(user_id: admin.id)}" } }
describe 'GET /api/v1/users' do
before { create_list(:user, 5) }
it 'returns paginated users for admin' do
get '/api/v1/users', headers: auth_headers
expect(response).to have_http_status(:ok)
expect(json_response['data']).to be_an(Array)
expect(json_response['pagination']).to include('current_page', 'total')
end
it 'returns 403 for non-admin' do
user = create(:user)
headers = { 'Authorization' => "Bearer #{JsonWebToken.encode(user_id: user.id)}" }
get '/api/v1/users', headers: headers
expect(response).to have_http_status(:forbidden)
end
end
describe 'POST /api/v1/users' do
let(:valid_params) { { user: { name: 'New User', email: 'new@example.com', password: 'SecurePass123!' } } }
it 'creates a new user' do
expect {
post '/api/v1/users', params: valid_params, headers: auth_headers
}.to change(User, :count).by(1)
expect(response).to have_http_status(:created)
end
end
def json_response
JSON.parse(response.body)
end
end
# spec/factories/users.rb
FactoryBot.define do
factory :user do
sequence(:email) { |n| "user#{n}@example.com" }
name { Faker::Name.name }
password { 'Password123!' }
role { :user }
is_active { true }
trait :admin do
role { :admin }
end
end
end
# config/initializers/rack_attack.rb
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
# app/controllers/api/v1/organizations_controller.rb
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