Axn - Unified Action DSL for Phoenix
A clean, step-based DSL library for defining actions that work seamlessly across Phoenix Controllers and LiveViews. Axn provides a unified interface for parameter validation, authorization, telemetry, and business logic where Plugs cannot be used.
Why Axn? Plugs only work with Plug.Conn but not Phoenix.LiveView.Socket. Axn bridges this gap, letting you write action logic once and use it in both contexts.
Installation
def deps do
[
{:axn, "~> 0.1.0"}
]
endQuick Start
defmodule MyApp.UserActions do
use Axn
action :create_user do
step :validate_params
step :require_admin
step :create_user
def validate_params(ctx) do
# Simple validation - details in docs
{:cont, ctx}
end
def require_admin(ctx) do
if admin?(ctx.assigns.current_user) do
{:cont, ctx}
else
{:halt, {:error, :unauthorized}}
end
end
def create_user(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"
end
end
# Use in Phoenix Controller
def create(conn, params) do
case MyApp.UserActions.run(:create_user, params, conn) do
{:ok, user} -> json(conn, %{user: user})
{:error, reason} -> json(conn, %{error: reason})
end
end
# Use in Phoenix LiveView
def handle_event("create_user", 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, "Error: #{reason}")}
end
endCore Concepts
Actions
Actions are named units of work that execute steps in order:
action :action_name do
step :step_name
step :step_name, option: value
step {ExternalModule, :external_step}
endSteps
Steps take a context and either continue or halt the pipeline:
def my_step(ctx) do
{:cont, updated_ctx} # Continue to next step
# OR
{:halt, {:ok, result}} # Stop with success
# OR
{:halt, {:error, reason}} # Stop with error
endContext
The Axn.Context struct flows through steps, carrying data:
%Axn.Context{
action: :create_user,
assigns: %{current_user: user}, # Phoenix-style assigns
params: %{email: "...", name: "..."}, # Request parameters
private: %{}, # Internal state
result: nil # Final result
}Built-in Steps
Parameter Validation
step :cast_validate_params, schema: %{
email!: :string, # Required
name: :string, # Optional
age: [field: :integer, default: 18] # With default
}
# With custom validation
step :cast_validate_params,
schema: %{phone!: :string},
validate: &validate_phone/1Authorization
Create simple authorization steps:
step :require_admin
def require_admin(ctx) do
if admin?(ctx.assigns.current_user) do
{:cont, ctx}
else
{:halt, {:error, :unauthorized}}
end
endTelemetry
Axn automatically emits telemetry events:
[:axn, :action, :start]- Action starts[:axn, :action, :stop]- Action completes[:axn, :action, :exception]- Action fails
Custom Metadata
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
endUnified Phoenix Integration
The Problem: Plugs work with Controllers but not LiveViews, creating code duplication.
The Solution: Axn works with both contexts seamlessly.
# Same action works in both:
MyApp.UserActions.run(:create_user, params, conn) # Controller
MyApp.UserActions.run(:create_user, params, socket) # LiveView
The action automatically extracts assigns from either conn or socket, eliminating the need to duplicate authorization, validation, and business logic.
Testing
test "create_user succeeds with valid input" do
assigns = %{current_user: %User{role: "admin"}}
params = %{"email" => "test@example.com", "name" => "John"}
assert {:ok, user} = MyApp.UserActions.run(:create_user, params, assigns)
assert user.email == "test@example.com"
endError Handling
Axn provides consistent error handling:
# Parameter errors
{:error, %{reason: :invalid_params, changeset: changeset}}
# Authorization errors
{:error, :unauthorized}
# Custom errors
{:error, :custom_reason}External Steps
Use steps from other modules:
action :complex_operation do
step :validate_params
step {MySteps, :enrich_context}, fields: [:preferences]
step :handle_operation
endPerformance
- Minimal overhead when telemetry is disabled
-
Efficient pipeline using
Enum.reduce_while/3 - Steps are pure functions, easy to optimize
Comparison
vs. Phoenix Plugs
- Plugs: Work only with Controllers (
Plug.Conn) - Axn: Works with both Controllers and LiveViews
vs. Phoenix Contexts
- Contexts: Business logic modules, manual integration
- Axn: Built-in Phoenix integration with parameter validation and telemetry
Contributing
- Fork the repository
-
Create your feature branch (
git checkout -b my-new-feature) - Add tests for your changes
-
Ensure all tests pass (
mix test) -
Run static analysis (
mix credo) -
Commit your changes (
git commit -am 'Add some feature') -
Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
MIT