Axn (Axn v0.2.0)
View SourceAxn - A clean, step-based DSL library for defining actions with parameter validation, authorization, telemetry, and custom business logic.
Axn provides a unified interface that works seamlessly across Phoenix Controllers
and LiveViews, solving the limitation that Plugs only work with Plug.Conn.
Core Features
- Step-based Pipeline: Define actions as a series of composable steps that execute in order
- Parameter Validation: Built-in schema-based parameter casting and validation using
Params - Authorization: Simple patterns for implementing custom authorization logic
- Telemetry: Automatic telemetry events with configurable metadata for monitoring
- Phoenix Integration: Works with both Controllers (Plug.Conn) and LiveViews (Phoenix.Socket)
- Error Handling: Consistent, structured error formats across all operations
- Composability: Steps can be reused across actions and even shared between modules
Key Concepts
Actions
Actions are named units of work that execute a series of steps in order. Each action automatically gets telemetry wrapping and error handling.
Steps
Steps are individual functions that take a context and either continue the pipeline
or halt it. Steps follow a simple contract: (ctx, opts) -> {:cont, new_ctx} | {:halt, result}.
Context
An Axn.Context struct flows through the step pipeline, carrying request data, user
information, and any step-added fields. Provides helper functions similar to Plug.Conn
and Phoenix.Component.
Quick Example
defmodule MyApp.UserActions do
use Axn
action :create_user do
step :cast_validate_params,
schema: %{email!: :string, name!: :string},
validate: &__MODULE__.validate_params/2
step :require_admin
step :handle_create
end
def validate_params(changeset, %{action: :create_user} = ctx) do
changeset
|> validate_format(:email, ~r/@/)
|> validate_admin_permissions(ctx.assigns.current_user)
end
def require_admin(ctx) do
if admin?(ctx.assigns.current_user) do
{:cont, ctx}
else
{:halt, {:error, :unauthorized}}
end
end
def handle_create(ctx) do
case Users.create(ctx.params) do
{:ok, user} -> {:halt, {:ok, user}}
{:error, reason} -> {:halt, {:error, reason}}
end
end
defp admin?(user), do: user && user.role == "admin"
endAdvanced Validation Example
Use pattern matching with context to handle multiple actions in a single validation function:
defmodule MyApp.AuthActions do
use Axn
action :request_otp do
step :cast_validate_params,
schema: %{phone!: :string, region: [field: :string, default: "US"]},
validate: &__MODULE__.validate_params/2
step :handle_request
end
action :verify_otp do
step :cast_validate_params,
schema: %{phone!: :string, code!: :string},
validate: &__MODULE__.validate_params/2
step :handle_verify
end
# Single validation function handling multiple actions
def validate_params(changeset, %{action: :request_otp} = ctx) do
params = Ecto.Changeset.apply_changes(changeset)
changeset
|> validate_phone_number(:phone, region: params.region)
|> validate_user_permissions(ctx.assigns.current_user)
end
def validate_params(changeset, %{action: :verify_otp} = ctx) do
changeset
|> validate_phone_number(:phone, region: "US")
|> validate_otp_format(:code)
|> validate_rate_limit(ctx.assigns.current_user)
end
endUsage in Phoenix
# Phoenix Controller
def create(conn, params) do
case MyApp.UserActions.run(:create_user, params, conn) do
{:ok, user} -> json(conn, %{success: true, user: user})
{:error, %{reason: :invalid_params, changeset: changeset}} ->
json(conn, %{errors: format_changeset_errors(changeset)})
{:error, reason} -> json(conn, %{error: reason})
end
end
# Phoenix LiveView
def handle_event("submit", params, socket) do
case MyApp.UserActions.run(:create_user, params, socket) do
{:ok, user} -> {:noreply, assign(socket, :user, user)}
{:error, reason} -> {:noreply, put_flash(socket, :error, reason)}
end
endDesign Principles
- Explicit over implicit: Each action clearly shows its execution flow
- Composable: Steps should be reusable across actions and modules
- Safe by default: Telemetry and error handling do not leak sensitive data
- Simple to implement: Minimal macro magic, straightforward execution model
- Easy to test: Steps are pure functions that are easy to unit test
- Familiar patterns: Feels natural to experienced Elixir developers
See the module documentation for Axn.Context for details on the context struct
and helper functions available in steps.
Summary
Functions
Sets up a module to use the Axn DSL for defining actions.
Defines an action with its steps.
Defines an action with options and its steps.
Defines a step within an action.
Functions
Sets up a module to use the Axn DSL for defining actions.
Options
:metadata- Function that takes a context and returns a map of custom metadata for telemetry events. Optional.
Examples
defmodule MyApp.UserActions do
use Axn
action :create_user do
# Action definition
end
end
defmodule MyApp.UserActions do
use Axn, metadata: &__MODULE__.telemetry_metadata/1
def telemetry_metadata(ctx) do
%{
user_id: ctx.assigns.current_user && ctx.assigns.current_user.id,
tenant: ctx.assigns.tenant && ctx.assigns.tenant.slug
}
end
end
Defines an action with its steps.
Examples
action :create_user do
step :cast_validate_params, schema: %{name!: :string}
step :authorize, &can_create_users?/1
step :handle_create
end
action :create_user, metadata: &action_metadata/1 do
step :cast_validate_params, schema: %{name!: :string}
step :handle_create
end
Defines an action with options and its steps.
Options
:metadata- Function that takes a context and returns a map of action-specific metadata for telemetry events. Optional.
Examples
action :create_user, metadata: &create_user_metadata/1 do
step :cast_validate_params, schema: %{name!: :string}
step :handle_create
end
Defines a step within an action.
Steps must implement one of these function signatures:
step_name(ctx)- Receives context onlystep_name(ctx, opts)- Receives context and step options
Return Values
Steps must return one of:
{:cont, updated_context}- Continue to next step with updated context{:halt, {:ok, result}}- Stop pipeline with success result{:halt, {:error, reason}}- Stop pipeline with error result
Examples
# Step with no options
step :my_step
# Step with options
step :my_step, option: value
# External step from another module
step {ExternalModule, :external_step}, option: value
# Built-in parameter validation step
step :cast_validate_params, schema: %{name!: :string, age: :integer}Step Implementation
# Simple step that just continues
def my_step(ctx) do
{:cont, Context.assign(ctx, :processed, true)}
end
# Step with options that modifies context
def my_step_with_options(ctx, opts) do
value = Keyword.get(opts, :option, "default")
{:cont, Context.assign(ctx, :custom_value, value)}
end
# Step that can halt the pipeline
def require_admin(ctx) do
if admin?(ctx.assigns.current_user) do
{:cont, ctx}
else
{:halt, {:error, :unauthorized}}
end
end
# Step that completes the action
def handle_create(ctx) do
case create_user(ctx.params) do
{:ok, user} -> {:halt, {:ok, user}}
{:error, reason} -> {:halt, {:error, reason}}
end
end