# Basic User.find(1) # raises if missing User.find_by(email: 'x') # nil if missing User.find_by!(email: 'x') # raises if missing User.first / User.last User.all # relation, not array # Dynamic finders User.find_by_email('x') # deprecated but asked # Find or create User.find_or_create_by(email: 'x') User.find_or_initialize_by(email: 'x')
# Conditions User.where(active: true) User.where("age > ?", 18) # safe User.where("age > #{age}") # β SQL injection! User.where.not(banned: true) User.where(role: ['admin', 'mod']) # IN # Order, limit, offset User.order('created_at DESC') User.order(created_at: :desc) User.limit(10).offset(20) # Aggregates User.count / .sum(:score) / .average(:age) User.minimum(:age) / .maximum(:age)
# N+1 killer Post.includes(:author, :comments) Post.eager_load(:author) # LEFT OUTER JOIN Post.preload(:author) # separate query # Joining (for filtering only) Post.joins(:comments).where(comments: { approved: true }) # Nested includes Post.includes(comments: :author) # Association queries user.posts.where(published: true) user.posts.create(title: 'Hi') user.posts.build(title: 'Hi') # unsaved
class Post < ApplicationRecord scope :published, -> { where(published: true) } scope :recent, -> { order(created_at: :desc) } scope :by_author, ->(id) { where(author_id: id) } # Default scope (use sparingly) default_scope { where(deleted_at: nil) } end # Chainable Post.published.recent.limit(5) Post.by_author(42).published # Unscope default Post.unscoped.where(id: 1)
# Create User.create(name: 'Eric') # save + return User.create!(name: 'Eric') # raises on invalid user = User.new(name: 'Eric') user.save # true/false user.save! # raises # Update user.update(name: 'New') # runs callbacks user.update_column(:name, 'x') # skips callbacks! User.update_all(active: false) # no callbacks # Delete user.destroy # runs callbacks user.delete # skips callbacks User.destroy_all(active: false) User.where(active: false).delete_all
# Transaction ActiveRecord::Base.transaction do account.debit(100) other.credit(100) raise ActiveRecord::Rollback # silent rollback end # Batch processing (memory-safe) User.find_each(batch_size: 500) do |user| user.do_something end User.find_in_batches(batch_size: 500) do |batch| process(batch) end
# Full REST (7 routes) resources :posts # Limit actions resources :posts, only: [:index, :show, :create] resources :posts, except: [:destroy] # Singular resource (no :id) resource :profile # show/edit/update/destroy # Nested resources :posts do resources :comments, only: [:create, :destroy] end # β /posts/:post_id/comments
# Member (has :id) vs Collection (no :id) resources :posts do member { post :publish } # /posts/:id/publish collection { get :search } # /posts/search end # Namespace (module + path prefix) namespace :api do namespace :v1 do resources :users end end # β /api/v1/users β Api::V1::UsersController # Scope (path only, no module) scope '/api/v1' do resources :users end
# Auto-generated path/url helpers posts_path # /posts post_path(@post) # /posts/1 new_post_path # /posts/new edit_post_path(@post) # /posts/1/edit posts_url # http://... full URL # Nested post_comments_path(@post) post_comment_path(@post, @comment) # Custom named route get 'sign_in', to: 'sessions#new', as: :login # β login_path
# Constraints get 'posts/:id', to: 'posts#show', constraints: { id: /\d+/ } # Route concern (DRY) concern :commentable do resources :comments end resources :posts, concerns: :commentable resources :articles, concerns: :commentable # Root & redirect root 'home#index' get '/old', to: redirect('/new')
| Verb | Path | Action | Helper | Use |
|---|---|---|---|---|
| GET | /posts | index | posts_path | List all |
| GET | /posts/new | new | new_post_path | New form |
| POST | /posts | create | posts_path | Create |
| GET | /posts/:id | show | post_path(@p) | Show one |
| GET | /posts/:id/edit | edit | edit_post_path(@p) | Edit form |
| PATCH/PUT | /posts/:id | update | post_path(@p) | Update |
| DELETE | /posts/:id | destroy | post_path(@p) | Delete |
class PostsController < ApplicationController before_action :authenticate_user! before_action :set_post, only: [:show, :edit, :update, :destroy] def index @posts = Post.all end def create @post = Post.new(post_params) if @post.save redirect_to @post, notice: 'Created!' else render :new, status: :unprocessable_entity end end private def set_post; @post = Post.find(params[:id]); end def post_params; params.require(:post).permit(:title, :body); end end
# require + permit (whitelist) params.require(:user).permit(:name, :email) # Nested attributes params.require(:post).permit( :title, tags: [], comments_attributes: [:body, :_destroy] ) # Optional require params.permit(:search) # β Never pass raw params to AR: # User.new(params[:user]) β mass assignment!
before_action # before action runs after_action # after (even on error) around_action # wraps with yield skip_before_action :auth, only: :index # Halting: render/redirect stops chain def check_admin redirect_to root_path unless current_user.admin? end # Respond to format respond_to do |format| format.html format.json { render json: @post } end
# Render (same request) render :new render json: { error: 'bad' }, status: :unprocessable_entity render plain: 'OK' head :no_content # 204, no body # Redirect (new request) redirect_to posts_path redirect_to @post redirect_to root_path, notice: 'Done' redirect_back fallback_location: root_path
# params params[:id] # route segment params[:q] # query string params[:post][:title] # form data # session (cookie-backed) session[:user_id] = user.id session.delete(:user_id) # flash (persists one request) flash[:notice] = 'Saved!' flash[:alert] = 'Error!' flash.now[:alert] = 'Now only' # current req only
rescue_from ActiveRecord::RecordNotFound, with: :not_found rescue_from Pundit::NotAuthorized do render json: { error: 'Forbidden' }, status: 403 end def not_found render json: { error: 'Not found' }, status: 404 end # Common status symbols # :ok 200 :created 201 :no_content 204 # :bad_request 400 :unauthorized 401 # :forbidden 403 :not_found 404 # :unprocessable_entity 422
class Api::V1::BaseController < ActionController::API before_action :authenticate! rescue_from ActiveRecord::RecordNotFound do render json: { error: 'Not found' }, status: 404 end private def authenticate! token = request.headers['Authorization'] &.split(' ')&.last @current_user = User.find_by!(token: token) end end # ActionController::API = no view layer # Use ::Base if you need sessions/views
# Simple render json: @user render json: @user, status: :created # Only / except render json: @user, only: [:id, :name] render json: @user, except: [:password_digest] # Include associations render json: @post, include: { comments: { only: [:id, :body] } } # Envelope + meta render json: { data: @users, meta: { total: @users.count, page: params[:page] } }
| Code | Rails | Meaning | Use |
|---|---|---|---|
| 200 | :ok | OK | GET success / generic success |
| 201 | :created | Created | POST created a resource |
| 204 | :no_content | No Content | Success with empty body (DELETE, PUT) |
| 400 | :bad_request | Bad Request | Malformed params / invalid JSON |
| 401 | :unauthorized | Unauthorized | Missing/invalid auth credentials |
| 403 | :forbidden | Forbidden | Authenticated but not allowed |
| 404 | :not_found | Not Found | Resource missing / wrong ID |
| 409 | :conflict | Conflict | State conflict (e.g., duplicate, versioning) |
| 422 | :unprocessable_entity | Unprocessable Entity | Validation failures |
| 429 | :too_many_requests | Too Many Requests | Rate limiting |
| 500 | :internal_server_error | Server Error | Unexpected exception |
400 for malformed input, 422 for valid input that fails validations.def as_json(options = {}) super(options.merge( only: [:id, :name, :email], methods: [:full_name], include: { role: { only: :name } } )) end # methods: adds virtual attr to output def full_name "#{first_name} #{last_name}" end # to_json calls as_json under the hood # render json: @obj calls as_json auto
# AMS (app/serializers/user_serializer.rb) class UserSerializer < ActiveModel::Serializer attributes :id, :name, :email has_many :posts def email object.email.downcase end end render json: @user # auto-uses serializer # Jbuilder (show.json.jbuilder) json.(@user, :id, :name) json.posts(@user.posts) do |post| json.(post, :id, :title) end
# Kaminari @posts = Post.page(params[:page]).per(25) @posts.total_count / @posts.current_page # will_paginate @posts = Post.paginate(page: params[:page], per_page: 25) # Response headers response.headers['X-Total-Count'] = @posts.total_count.to_s response.headers['X-Page'] = params[:page] # Content request.format # :html / :json / :xml request.xhr? # AJAX?
# rack-cors (config/initializers/cors.rb) Rails.application.config.middleware .insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :patch, :delete] end end # Versioning strategies # 1. URL: /api/v1/posts β most common # 2. Header: Accept: application/vnd.v1+json # 3. Subdomain: api.v1.myapp.com
class User < ApplicationRecord validates :name, presence: true, length: { minimum: 2, maximum: 50 } validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP } validates :age, numericality: { greater_than: 0 } validates :role, inclusion: { in: %w[admin user guest] } validates :bio, length: { maximum: 500 }, allow_blank: true validate :custom_check def custom_check errors.add(:base, 'bad') if bad? end end
class Post < ApplicationRecord belongs_to :user belongs_to :category, optional: true has_many :comments, dependent: :destroy has_many :commenters, through: :comments, source: :user has_one :image, dependent: :destroy has_many :tags, through: :post_tags end # dependent: options # :destroy β calls destroy (callbacks run) # :delete_all β SQL DELETE, no callbacks # :nullify β sets FK to null # :restrict_with_error β blocks destroy
before_validation / after_validation before_save # create AND update before_create / after_create after_save before_update / after_update before_destroy / after_destroy after_commit # after DB tx commits β use for jobs after_rollback # Examples before_save :normalize_email after_create :send_welcome_email after_commit :enqueue_job, on: :create
# Generate rails g migration AddSlugToPosts slug:string:uniq rails g migration CreatePosts title:string user:references # Methods add_column :posts, :slug, :string, null: false, default: '' remove_column :posts, :old_field rename_column :posts, :old, :new_name change_column :posts, :views, :bigint add_index :users, :email, unique: true add_reference :posts, :author, foreign_key: { to_table: :users } # Types: string text integer bigint float # decimal boolean datetime date jsonb
# After save attempt user.valid? # runs validations, returns bool user.invalid? user.errors.full_messages # ["Name can't be blank"] user.errors[:email] # ["is invalid"] user.errors.any? / user.errors.count # In controller β respond after failed save if @user.save render json: @user, status: :created else render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity end
class CreatePost def initialize(user:, params:) @user, @params = user, params end def call post = @user.posts.build(@params) post.save! NotifyJob.perform_later(post) post end end # In controller @post = CreatePost.new( user: current_user, params: post_params ).call
class WelcomeEmailJob < ApplicationJob queue_as :default def perform(user_id) user = User.find(user_id) UserMailer.welcome(user).deliver_now end end # Enqueue WelcomeEmailJob.perform_later(user.id) WelcomeEmailJob.set(wait: 5.minutes) .perform_later(user.id) # β Pass IDs not objects # Backends: Sidekiq, Delayed::Job, Resque
# Low-level Rails.cache.fetch("user/#{id}/stats", expires_in: 12.hours) do User.find(id).compute_stats end Rails.cache.write('key', val) Rails.cache.read('key') Rails.cache.delete('key') # HTTP caching expires_in 1.hour, public: true fresh_when @post # sets ETag + Last-Modified stale?(@post) # check & set headers
module Trackable extend ActiveSupport::Concern included do before_create :set_uuid scope :recent, -> { order(created_at: :desc) } end def set_uuid self.uuid = SecureRandom.uuid end class_methods do def find_by_uuid(uuid) = find_by!(uuid: uuid) end end class Post < ApplicationRecord include Trackable end
# Safe navigation user&.profile&.avatar_url # Memoize @result ||= expensive_call # Presence / blank " ".blank? # true (whitespace = blank) "".present? # false nil.presence # nil (returns nil if blank) "x".presence # "x" # Shortcuts [1,2,3].map(&:to_s) # symbol to proc %w[foo bar baz] # word array %i[foo bar baz] # symbol array hash.fetch(:key, default)
# Env checks Rails.env.production? Rails.env.development? Rails.env.test? # Credentials (Rails 5.2+) Rails.application.credentials.secret_key Rails.application.credentials.dig(:aws, :key) # ENV vars ENV.fetch('DATABASE_URL') # raises if missing ENV.fetch('PORT', '3000') # with default # Route helpers outside views include Rails.application.routes.url_helpers