ruby 80 lines · 3 tabs

Attach and Resize an Avatar with an Active Storage Variant in Rails

Shared by codesnips Aug 2026
3 tabs
class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumb,
      resize_to_limit: [256, 256],
      convert: :webp,
      saver: { quality: 80 }
  end

  validate :acceptable_avatar

  after_commit :prewarm_avatar, on: %i[create update], if: -> { avatar.attached? && saved_changes? }

  def avatar_thumb
    avatar.variant(:thumb)
  end

  private

  def acceptable_avatar
    return unless avatar.attached?

    unless avatar.blob.content_type.in?(%w[image/png image/jpeg image/webp])
      errors.add(:avatar, "must be a PNG, JPEG, or WebP")
    end

    if avatar.blob.byte_size > 8.megabytes
      errors.add(:avatar, "is too large (max 8 MB)")
    end
  end

  def prewarm_avatar
    AvatarPrewarmJob.perform_later(self)
  end
end
3 files · ruby Explain with highlit

This snippet shows the idiomatic Rails way to accept an avatar upload, validate it, and serve a resized variant without ever storing a second file up front. Active Storage variants are computed lazily and cached in the same service (S3, disk, GCS), so the model only declares how the image should be transformed while the blob stays authoritative.

In User model, has_one_attached :avatar wires up the association, and a named avatar_thumb block centralizes the transformation via variant. Using a named variant keeps the resize spec (resize_to_limit, convert: :webp, saver quality) in one place so views and jobs never repeat magic numbers. The resize_to_limit transform preserves aspect ratio and never upscales, which is the correct choice for user-supplied images of unknown dimensions. acceptable_avatar runs on save and rejects blobs by content_type and byte_size before anything is processed — validating the blob rather than the variant matters because variant generation is expensive and untrusted input should be rejected early.

In AvatarsController, update calls attach on the association and relies on the model validation to gate the write; avatar.attach stages the blob, and save triggers the callback. On failure the controller re-renders with unprocessable_entity so form errors surface normally. The show-style redirect uses rails_representation_url to hand back a URL for the processed variant rather than streaming bytes through the app.

The enqueue avatar prewarm concern demonstrates a common production refinement: because variants are generated on first request, the very first viewer pays the resize cost. after_commit enqueues AvatarPrewarmJob which calls processed on the variant to force generation ahead of time, warming the cache. This trades a little background work for predictable request latency.

A key pitfall is calling variant in a hot loop or in views without the named helper — each call re-derives the key; the named variant and processed avoid redundant work. Reaching for this pattern makes sense whenever images are user-uploaded, must be normalized to a safe format and size, and should be served cheaply from object storage.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Attach and Resize an Avatar with an Active Storage Variant in Rails — share card
Link copied