ruby 89 lines · 3 tabs

Presenter + fast_jsonapi Serializer for Clean JSON:API Responses in Rails

Shared by codesnips Jul 2026
3 tabs
class OrderPresenter < SimpleDelegator
  attr_reader :current_user

  def initialize(order, current_user:)
    super(order)
    @current_user = current_user
  end

  def self.wrap(orders, current_user:)
    orders.map { |o| new(o, current_user: current_user) }
  end

  def order
    __getobj__
  end

  def status_label
    status.to_s.humanize
  end

  def total_formatted
    format("$%.2f", total_cents / 100.0)
  end

  def can_cancel?
    return false if current_user.nil?
    pending? && (current_user.admin? || current_user.id == customer_id)
  end
end
3 files · ruby Explain with highlit

This snippet shows how to keep controllers thin and JSON output stable by combining two complementary patterns: a presenter (view model) that computes derived, request-aware fields, and a fast_jsonapi-style serializer that shapes the wire format. The two live at different layers — the presenter owns what a record looks like for a given viewer, and the serializer owns how that gets encoded into a JSON:API document — so neither leaks concerns into the controller.

In OrderPresenter, an ActiveRecord Order is wrapped in a plain Ruby object built with SimpleDelegator. Delegating means the presenter transparently exposes the model's own attributes while adding presentation-only methods like status_label, total_formatted, and can_cancel?. It also carries the current_user so authorization-sensitive fields (can_cancel?) can depend on the viewer without the model reaching into request state. This is the key trade-off of the presenter pattern: a little indirection buys a place to put formatting and policy logic that would otherwise pollute models or views.

In OrderSerializer, the JSONAPI::Serializer DSL declares the resource type, its attributes, and relationships. Because the serializer is fed presenter instances, attribute :status_label and attribute :total resolve against presenter methods rather than raw columns, so the JSON:API contract stays decoupled from the database schema. The belongs_to :customer line emits a proper relationship object with resource linkage, which clients can use with include.

In Api::V1::OrdersController, the index and show actions wrap records via a private present helper before serialization. Passing params: { current_user: current_user } threads request context into the serializer so it can conditionally include fields, and include: lets callers request compound documents. Wrapping collections happens in memory, so for large result sets pagination (via page) matters to avoid instantiating thousands of presenters.

The payoff is a stable, testable boundary: the serializer is a pure function of presenters, presenters are cheap to unit test without HTTP, and the controller does almost nothing. When output needs to change, edits stay local — formatting in the presenter, structure in the serializer — instead of rippling through views or model callbacks.


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
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

jwt authentication api
by Kai Nakamura 2 tabs
typescript
export interface RetryOptions {
  retries: number;
  baseMs: number;
  maxMs: number;
  signal?: AbortSignal;
  onRetry?: (attempt: number, delay: number, err: unknown) => void;

Exponential backoff with jitter for retries

typescript reliability retry
by codesnips 2 tabs
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

Share this code

Here's the card — post it anywhere.

Presenter + fast_jsonapi Serializer for Clean JSON:API Responses in Rails — share card
Link copied