Controllers
Servus provides controller helpers that eliminate the success/failure branching boilerplate. The Railtie auto-includes Servus::Helpers::ControllerHelpers into all controllers — no setup needed.
run_service
run_service calls the service, stores the result in @result, and automatically renders a JSON error response on failure. It also returns the result, so you can branch on it:
class Api::V1::TreasuryTransfersController < ApiController
def create
result = run_service(Treasury::TransferGold::Service, transfer_params)
return unless result.success?
render json: result.data, status: :created
end
private
def transfer_params
params.permit(:from_account, :to_account, :gold_dragons)
end
endOn failure, the controller renders the error automatically using the error's http_status and api_error:
// failure("Account not found", type: NotFoundError)
// renders with status 404:
{
"error": {
"code": "not_found",
"message": "Account not found"
}
}This is where typed errors pay off — a failure with type: NotFoundError becomes a 404, type: ForbiddenError becomes a 403, and so on. The controller doesn't interpret the failure; the error type carries its own HTTP semantics.
@result is always set — on both success and failure. Use it in views, serializers, or anywhere in the request lifecycle after run_service is called.
Orchestrating multiple services
When a controller action needs to call multiple services and coordinate their results, skip run_service and call the services directly:
def create
reserve = Treasury::ReserveFunds::Service.call(**reserve_params)
return render_service_error(reserve.error) unless reserve.success?
dispatch = Ravens::DispatchReceipt::Service.call(transfer: reserve.data.transfer_id)
return render_service_error(dispatch.error) unless dispatch.success?
render json: { reserve: reserve.data, dispatch: dispatch.data }, status: :created
endUsing @result in views
On success, @result holds the response. Use it in your view or serializer:
class Api::V1::TreasuryTransfersController < ApiController
def create
run_service(Treasury::TransferGold::Service, transfer_params)
end
# app/views/api/v1/treasury_transfers/create.json.jbuilder
# json.transferred @result.data.transferred
# json.from_balance @result.data.from_balance
# json.to_balance @result.data.to_balance
endOr render inline:
def create
result = run_service(Treasury::TransferGold::Service, transfer_params)
return unless result.success?
render json: @result.data, status: :created
endrender_service_error
run_service delegates failure rendering to render_service_error, which you can override to customize the error response format:
class ApplicationController < ActionController::Base
private
def render_service_error(error)
render json: {
error: {
type: error.api_error[:code],
message: error.message,
timestamp: Time.current
}
}, status: error.http_status
end
endThe default implementation renders { error: error.api_error } with error.http_status.