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.

June 8, 2025by PromptGenius Team
railsrubycursor-rulesbackendmvcactive-record
Ruby on Rails Cursor Rules: Rapid Backend Development

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