Ruby on Rails Cursor Rules: Rapid Backend Development
Cursor rules for Ruby on Rails covering MVC patterns, ActiveRecord, RESTful routing, Strong Parameters, background jobs, and RSpec testing.

Overview
Ruby on Rails is the convention-over-configuration web framework that powers millions of applications from startups to enterprises. These cursor rules enforce MVC separation, ActiveRecord best practices, RESTful routing conventions, Strong Parameters for mass assignment protection, and RSpec testing patterns to help AI assistants generate idiomatic Rails code.
Note:
Enforces skinny controllers, fat models, service objects for complex logic, RESTful resource routing, Strong Parameters, ActiveJob for background work, and RSpec/FactoryBot testing conventions.
Rules Configuration
---
description: Enforces idiomatic Rails patterns including MVC separation, ActiveRecord conventions, RESTful routing, Strong Parameters, background jobs, and RSpec testing. Provides guidelines for maintainable, convention-driven Ruby backends.
globs: **/*.rb,**/*.erb,**/*.rake,**/*.yml
---
# Ruby on Rails Best Practices
You are an expert in Ruby on Rails, Ruby, and backend web development.
You understand MVC architecture, ActiveRecord, REST API design, and production deployment.
### Project Structure
- /app/models — ActiveRecord models with validations, associations, scopes
- /app/controllers — thin controllers that delegate to services and models
- /app/services — Plain Old Ruby Objects (POROs) for business logic
- /app/jobs — ActiveJob classes for background processing
- /app/serializers — JSON serialization (ActiveModelSerializers or jbuilder)
- /config/routes.rb — RESTful resource declarations
- /spec — RSpec test files mirroring app structure
### MVC & Controllers
- Keep controllers skinny: only handle params, call services/models, render response
- Use Strong Parameters: params.require(:model).permit(:fields)
- Handle record-not-found with rescue_from ActiveRecord::RecordNotFound
- Return JSON responses with render json: serializer or jbuilder templates
- Use before_action for auth checks and shared setup
- Never call model methods that mutate data from views or templates
### ActiveRecord & Models
- Define validations with validates :field, presence: true, uniqueness: true, etc.
- Use associations: belongs_to, has_many, has_one, has_many :through
- Add database indexes for foreign keys and frequently queried columns in migrations
- Use scopes for common query chains: scope :active, -> { where(active: true) }
- Limit callbacks to simple state changes (e.g., setting defaults, timestamps); use service objects for complex business logic
- Use create! and update! (bang methods) to raise on failure when appropriate
- Prefer find_each over each for iterating large result sets
### Routing & APIs
- Use resources :posts, only: [:index, :show, :create, :update, :destroy]
- Namespace API routes: namespace :api { namespace :v1 { resources :posts } }
- Use member and collection blocks for custom actions: member { post :publish }
- Return consistent JSON envelopes: { data:, meta: { page, total, per_page } }
- Use proper HTTP status codes (200, 201, 204, 400, 401, 403, 404, 422)
### Background Jobs
- Use ActiveJob with Sidekiq adapter for async processing
- Make jobs idempotent with unique job keys
- Keep job arguments simple (avoid passing entire ActiveRecord objects)
- Set retry limits and handle failures with discard_on or retry_on
- Use deliver_later for all outbound emails
### Security
- Enable CSRF protection (default in ApplicationController)
- Use has_secure_password for bcrypt password hashing
- Store secrets in Rails credentials or environment variables
- Avoid mass assignment vulnerabilities with Strong Parameters
- Sanitize user input before rendering in views
### Testing
- Use RSpec with FactoryBot for test data
- Write request specs for API endpoints
- Write model specs for validations, associations, and scopes
- Use shoulda-matchers for concise model tests
- Mock external services with WebMock or VCR
- Run tests with RAILS_ENV=test
Installation
Create rails.mdc in your project's .cursor/rules/ directory and paste the configuration above. Cursor and Windsurf both read .cursor/rules/ — Copilot users place it in .github/copilot-instructions.md instead.
Examples
# app/controllers/api/v1/posts_controller.rb
module Api
module V1
class PostsController < ApplicationController
before_action :authenticate_user!
before_action :set_post, only: [:show, :update, :destroy]
def index
posts = Post.includes(:user)
.published
.page(params[:page])
.per(params[:per_page] || 20)
render json: {
data: PostSerializer.new(posts).serializable_hash,
meta: pagination_meta(posts)
}
end
def create
result = Posts::CreateService.new(current_user, post_params).call
if result.success?
render json: PostSerializer.new(result.post).serializable_hash,
status: :created
else
render json: { errors: result.errors }, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: 'Post not found' }, status: :not_found
end
def post_params
params.require(:post).permit(:title, :body, :published)
end
def pagination_meta(collection)
{
page: collection.current_page,
total: collection.total_count,
per_page: collection.limit_value
}
end
end
end
end
# app/models/post.rb
class Post < ApplicationRecord
belongs_to :user
has_many :comments, dependent: :destroy
has_many :taggings, dependent: :destroy
has_many :tags, through: :taggings
validates :title, presence: true, length: { maximum: 200 }
validates :body, presence: true
scope :published, -> { where(published: true).order(created_at: :desc) }
scope :by_user, ->(user) { where(user: user) }
def publish!
update!(published: true)
end
end
# spec/requests/api/v1/posts_spec.rb
require 'rails_helper'
RSpec.describe 'Api::V1::Posts', type: :request do
let(:user) { create(:user) }
let(:headers) { auth_headers_for(user) }
describe 'GET /api/v1/posts' do
before { create_list(:post, 3, user: user, published: true) }
it 'returns published posts with pagination meta' do
get '/api/v1/posts', headers: headers
expect(response).to have_http_status(:ok)
json = JSON.parse(response.body)
expect(json['data'].length).to eq(3)
expect(json['meta']).to include('page', 'total', 'per_page')
end
end
describe 'POST /api/v1/posts' do
let(:valid_params) { { post: { title: 'Hello', body: 'World' } } }
it 'creates a post and returns 201' do
post '/api/v1/posts', params: valid_params, headers: headers
expect(response).to have_http_status(:created)
json = JSON.parse(response.body)
expect(json['data']['title']).to eq('Hello')
end
it 'returns 422 with errors for invalid data' do
post '/api/v1/posts', params: { post: { title: '' } }, headers: headers
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
Related Resources
Related Articles
Kotlin Cursor Rules: Android Development Best Practices
Cursor rules for Kotlin and Android enforcing Jetpack Compose, coroutines, and clean architecture. Helps AI generate secure, production-ready code with context.
Go Cursor Rules: AI-Powered Development Best Practices
Cursor rules for Go development enforcing idiomatic patterns, modern Go 1.21+ features, and clean code principles with AI assistance for production-ready code.
C/C++ Cursor Rules: Systems Programming Configuration
Cursor rules for C and C++ development covering memory management, STL usage, RAII patterns, and modern C++ standards for systems-level programming.