class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :excerpt, :body, :published_at, :views, :likes_count, :comments_count
attribute :can_edit, if: :current_user_can_edit?
belongs_to :author, serializer: UserSummarySerializer
has_many :comments, if: :include_comments?
def excerpt
object.body&.truncate(200)
end
def current_user_can_edit?
scope && (scope.id == object.author_id || scope.admin?)
end
def include_comments?
instance_options[:include_comments] == true
end
end
class UserSummarySerializer < ActiveModel::Serializer
attributes :id, :name, :avatar_url, :profile_url
def profile_url
Rails.application.routes.url_helpers.user_path(object)
end
end
Controllers shouldn't know about JSON structure—that's a serialization concern. ActiveModel::Serializers (AMS) separates presentation from business logic by defining dedicated serializer classes for each model. Serializers specify exactly which attributes to expose and handle nested associations cleanly. I can create different serializers for different contexts (list view vs detail view) without cluttering controllers. The if: option conditionally includes fields based on permissions or feature flags. AMS also supports meta attributes and links for JSONAPI-compliant responses. For high-performance APIs, I sometimes use simpler solutions like Jbuilder or custom PORO serializers, but AMS provides good defaults with minimal boilerplate for standard REST APIs.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
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
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.