Loading...
Loading...
Rails 7+ framework guardrails, patterns, and best practices for AI-assisted development. Use when working with Rails projects, or when the user mentions Ruby on Rails. Provides ActiveRecord, Hotwire/Turbo, Action Cable, and convention-over-configuration guidelines.
npx skill4agent add ar4mirez/samuel railsApplies to: Rails 7+, Ruby 3.2+, Hotwire/Turbo, ActiveRecord, Action Cable
myapp/
├── app/
│ ├── controllers/
│ │ ├── application_controller.rb
│ │ ├── concerns/ # Controller concerns (auth, pagination)
│ │ └── api/
│ │ └── v1/ # Versioned API controllers
│ ├── models/
│ │ ├── application_record.rb
│ │ └── concerns/ # Model concerns (searchable, sluggable)
│ ├── views/
│ │ ├── layouts/ # Application layouts
│ │ │ └── application.html.erb
│ │ ├── shared/ # Shared partials (_navbar, _footer)
│ │ └── posts/ # Resource-specific views
│ ├── helpers/ # View helpers
│ ├── jobs/ # ActiveJob classes
│ ├── mailers/ # Action Mailer classes
│ ├── channels/ # Action Cable channels
│ └── services/ # Service objects (custom directory)
├── config/
│ ├── routes.rb # Route definitions
│ ├── database.yml # Database configuration
│ ├── environments/ # Per-environment settings
│ └── initializers/ # Boot-time configuration
├── db/
│ ├── migrate/ # Migration files
│ ├── schema.rb # Current schema snapshot
│ └── seeds.rb # Seed data
├── lib/
│ └── tasks/ # Custom Rake tasks
├── test/ # Minitest (default) or spec/ for RSpec
│ ├── models/
│ ├── controllers/
│ ├── integration/
│ └── system/
├── Gemfile
└── Gemfile.lockapp/services/Api::V1views/shared/before_action*_paramsstatus: :unprocessable_entitystatus: :see_otherredirect_torespond_todependent:has_manyhas_onescopeenumbefore_validationcounter_cache: truenull: falsereferencesforeign_key: truedefault:updownparams.permit!rawhtml_safeRails.application.credentialsforce_sslcontent_security_policyincludespreloadeager_loadcounter_cachecache @record dofind_eacheach# app/models/post.rb
class Post < ApplicationRecord
# 1. Associations
belongs_to :user
belongs_to :category, optional: true
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
has_one_attached :featured_image
# 2. Validations
validates :title, presence: true, length: { maximum: 255 }
validates :body, presence: true
# 3. Enums
enum :status, { draft: 0, published: 1, archived: 2 }
# 4. Scopes
scope :published, -> { where(status: :published) }
scope :recent, -> { order(created_at: :desc) }
scope :by_category, ->(cat) { where(category: cat) }
scope :search, ->(q) {
where("title ILIKE :q OR body ILIKE :q", q: "%#{sanitize_sql_like(q)}%")
}
# 5. Callbacks (keep minimal)
before_validation :normalize_title
# 6. Instance methods
def publish!
update!(status: :published, published_at: Time.current)
end
private
def normalize_title
self.title = title&.strip
end
end# app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_post, only: [:show, :edit, :update, :destroy]
before_action :authorize_post!, only: [:edit, :update, :destroy]
def index
@posts = Post.published
.includes(:user, :category)
.recent
.page(params[:page])
.per(20)
end
def show; end
def new
@post = current_user.posts.build
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: "Post created."
else
render :new, status: :unprocessable_entity
end
end
def edit; end
def update
if @post.update(post_params)
redirect_to @post, notice: "Post updated."
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@post.destroy
redirect_to posts_url, notice: "Post deleted.", status: :see_other
end
private
def set_post
@post = Post.find(params[:id])
end
def authorize_post!
redirect_to posts_url, alert: "Not authorized." unless @post.user == current_user
end
def post_params
params.require(:post).permit(:title, :body, :category_id, :status, :featured_image, tag_ids: [])
end
end_form.html.erbneweditrender @posts_post.html.erbrender partial: "post", locals: { post: @post }content_for :title# config/routes.rb
Rails.application.routes.draw do
root "home#index"
# Authentication
get "login", to: "sessions#new"
post "login", to: "sessions#create"
delete "logout", to: "sessions#destroy"
# RESTful resources
resources :posts do
resources :comments, only: [:create, :destroy]
member do
post :publish
post :unpublish
end
end
resources :categories, only: [:index, :show] do
resources :posts, only: [:index]
end
# Admin namespace
namespace :admin do
root "dashboard#index"
resources :users
resources :posts
end
# API namespace
namespace :api do
namespace :v1 do
resources :posts, only: [:index, :show, :create, :update, :destroy]
end
end
# Health check
get "health", to: "health#show"
endresourcesonly:except:membercollectionshallow: true# db/migrate/20240115000000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.1]
def change
create_table :posts do |t|
t.references :user, null: false, foreign_key: true
t.references :category, foreign_key: true
t.string :title, null: false
t.text :body, null: false
t.integer :status, default: 0, null: false
t.datetime :published_at
t.integer :comments_count, default: 0, null: false
t.timestamps
end
add_index :posts, [:user_id, :status]
add_index :posts, :published_at
end
endt.referencest.timestampscreated_atupdated_atcomments_countcounter_cache: true# app/services/application_service.rb
class ApplicationService
def self.call(...)
new(...).call
end
end
# app/services/posts/publish_service.rb
module Posts
class PublishService < ApplicationService
def initialize(post:, user:)
@post = post
@user = user
end
def call
return ServiceResult.failure(["Not authorized"]) unless @user == @post.user
ActiveRecord::Base.transaction do
@post.update!(status: :published, published_at: Time.current)
notify_subscribers
end
ServiceResult.success(@post)
rescue ActiveRecord::RecordInvalid => e
ServiceResult.failure(e.record.errors.full_messages)
end
private
def notify_subscribers
PostMailer.published(@post).deliver_later
end
end
endself.call(...)ActiveRecord::Base.transactionPosts::PublishServiceUsers::CreateService# Application
rails new myapp --database=postgresql --css=tailwind
rails new myapp --api # API-only mode
rails server # Start dev server
rails console # Interactive console
rails routes # List all routes
# Generators
rails generate model User name:string email:string
rails generate controller Posts index show new create
rails generate scaffold Article title:string body:text
rails generate migration AddStatusToPosts status:integer
# Database
rails db:create # Create database
rails db:migrate # Run pending migrations
rails db:rollback # Undo last migration
rails db:seed # Run seeds.rb
rails db:reset # Drop, create, migrate, seed
# Testing
rails test # Run all tests
rails test:models # Model tests only
rails test:system # System tests only
# Assets and dependencies
bundle install # Install gems
rails assets:precompile # Compile assets for production# test/models/post_test.rb
require "test_helper"
class PostTest < ActiveSupport::TestCase
def setup
@post = posts(:first_post)
end
test "valid post" do
assert @post.valid?
end
test "invalid without title" do
@post.title = nil
assert_not @post.valid?
assert_includes @post.errors[:title], "can't be blank"
end
test "publish! sets status and timestamp" do
@post.publish!
assert @post.published?
assert_not_nil @post.published_at
end
endfactory_bottest "user cannot edit others' posts"rails ~> 7.1pgpumaredisturbo-railsstimulus-railsimportmap-railsbcryptsidekiqkaminaripagydebugcapybaraselenium-webdriverweb-consolerack-mini-profilerrspec-railsfactory_bot_railsfakershoulda-matcherswebmockincludespreloadfind_eachparams.permit!Model.allrawhtml_safe