Zodra

Controllers

Use the Zodra::Controller mixin for validated params and typed responses

Controllers

The Zodra::Controller mixin connects your Rails controllers to contracts, providing validated params and structured responses.

Setup

app/controllers/api/v1/products_controller.rb
module Api
  module V1
    class ProductsController < ApplicationController
      include Zodra::Controller
    end
  end
end

Zodra infers the contract name from the controller class: ProductsController:products.

zodra_contract

Use zodra_contract to override the inferred name when the controller and contract names don't match:

zodra_contract :inventory  # uses the :inventory contract instead of :products

zodra_params

Returns the validated and coerced request params, parsed according to the action's params definition:

def create
  params = zodra_params
  # params is a hash with validated, typed values
  # Unknown keys raise an error (when strict_params is true)
  product = Product.create!(params)
  zodra_respond(product, status: :created)
end

For actions without a params definition, zodra_params returns an empty hash.

zodra_respond

Renders a single object wrapped in { data: ... }:

def show
  product = Product.find(zodra_params[:id])
  zodra_respond(product)
  # Response: { data: { id: "...", name: "...", ... } }
end

With a custom status:

zodra_respond(product, status: :created)

For no-content responses:

def destroy
  Product.find(zodra_params[:id]).destroy!
  head :no_content
end

zodra_respond_collection

Renders an array wrapped in { data: [...] }:

def index
  products = Product.all
  zodra_respond_collection(products)
  # Response: { data: [...] }
end

With optional meta:

zodra_respond_collection(products, meta: { total: products.count })

zodra_rescue

Maps exceptions to error codes defined in the contract:

class OrdersController < ApplicationController
  include Zodra::Controller

  zodra_rescue :confirm, InvalidTransitionError, as: :invalid_transition
  zodra_rescue :cancel, InvalidTransitionError, as: :invalid_transition

  def confirm
    order = Order.find(zodra_params[:id])
    order.confirm!
    zodra_respond(order.reload)
  end
end

When InvalidTransitionError is raised, Zodra renders:

{
  "error": {
    "code": "invalid_transition",
    "message": "..."
  }
}

The HTTP status comes from the error definition in the contract.

See Error Handling for the full error flow.

zodra_errors

Returns field-level validation errors. Accepts ActiveModel::Errors, hashes, or anything responding to .messages:

def create
  product = Product.new(zodra_params)

  if product.save
    zodra_respond(product, status: :created)
  else
    zodra_errors(product.errors)
  end
end

Keys are automatically transformed to match your key_format configuration, including nested keys in arrays.

For complex error mapping (multiple models, key remapping), use Zodra::ErrorMapper. See Error Handling.

Full controller example

app/controllers/api/v1/customers_controller.rb
module Api
  module V1
    class CustomersController < ApplicationController
      include Zodra::Controller

      def index
        zodra_respond_collection(Customer.all)
      end

      def show
        zodra_respond(Customer.find(zodra_params[:id]))
      end

      def create
        customer = Customer.new(zodra_params)

        if customer.save
          zodra_respond(customer, status: :created)
        else
          zodra_errors(customer.errors)
        end
      end

      def update
        customer = Customer.find(zodra_params[:id])
        customer.assign_attributes(zodra_params.except(:id))

        if customer.save
          zodra_respond(customer)
        else
          zodra_errors(customer.errors)
        end
      end

      def destroy
        Customer.find(zodra_params[:id]).destroy!
        head :no_content
      end
    end
  end
end

On this page