From elixir-phoenix-guide
Enforces Elixir best practices like pattern matching over if/else, pipe operator for chaining, with for sequential fallible ops, @impl true, and let-it-crash when editing .ex/.exs files.
npx claudepluginhub j-morgan6/elixir-phoenix-guide --plugin elixir-phoenix-guideThis skill uses the workspace's default tool permissions.
1. **Use pattern matching over if/else** for control flow and data extraction
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Checks Next.js compilation errors using a running Turbopack dev server after code edits. Fixes actionable issues before reporting complete. Replaces `next build`.
with for 2+ sequential fallible operations instead of nested case?, dangerous functions end with !Pattern matching is the primary control flow mechanism in Elixir. Prefer it over conditional statements.
Bad:
def process(result) do
if result.status == :ok do
result.data
else
nil
end
end
Good:
def process(%{status: :ok, data: data}), do: data
def process(_), do: nil
Bad:
def handle_response(response) do
if response.status == 200 do
{:ok, response.body}
else if response.status == 404 do
{:error, :not_found}
else
{:error, :unknown}
end
end
Good:
def handle_response(%{status: 200, body: body}), do: {:ok, body}
def handle_response(%{status: 404}), do: {:error, :not_found}
def handle_response(_), do: {:error, :unknown}
Use the pipe operator |> to chain function calls for improved readability.
Bad:
String.upcase(String.trim(user_input))
Good:
user_input
|> String.trim()
|> String.upcase()
Bad:
def process_user(user) do
validated = validate_user(user)
transformed = transform_user(validated)
save_user(transformed)
end
Good:
def process_user(user) do
user
|> validate_user()
|> transform_user()
|> save_user()
end
Use with for sequential operations that can fail.
Bad:
def create_post(params) do
case validate_params(params) do
{:ok, valid_params} ->
case create_changeset(valid_params) do
{:ok, changeset} ->
Repo.insert(changeset)
error -> error
end
error -> error
end
end
Good:
def create_post(params) do
with {:ok, valid_params} <- validate_params(params),
{:ok, changeset} <- create_changeset(valid_params),
{:ok, post} <- Repo.insert(changeset) do
{:ok, post}
end
end
Handle specific errors in the else block.
def transfer_money(from_id, to_id, amount) do
with {:ok, from_account} <- get_account(from_id),
{:ok, to_account} <- get_account(to_id),
:ok <- validate_balance(from_account, amount),
{:ok, _} <- debit(from_account, amount),
{:ok, _} <- credit(to_account, amount) do
{:ok, :transfer_complete}
else
{:error, :insufficient_funds} ->
{:error, "Not enough money in account"}
{:error, :not_found} ->
{:error, "Account not found"}
error ->
{:error, "Transfer failed: #{inspect(error)}"}
end
end
Use guards for simple type and value checks in function heads.
def calculate(x) when is_integer(x) and x > 0 do
x * 2
end
def calculate(_), do: {:error, :invalid_input}
Use for comprehensions for complex transformations and filtering.
Bad (multiple passes):
list
|> Enum.map(&transform/1)
|> Enum.filter(&valid?/1)
|> Enum.map(&format/1)
Good (single pass):
for item <- list,
transformed = transform(item),
valid?(transformed) do
format(transformed)
end
PascalCasesnake_casesnake_case:snake_case?: valid?, empty?!: save!, update!The idiomatic way to handle success and failure in Elixir.
def fetch_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
# Usage
case fetch_user(123) do
{:ok, user} -> IO.puts("Found: #{user.name}")
{:error, :not_found} -> IO.puts("User not found")
end
Pattern match on results.
def process_upload(file) do
case save_file(file) do
{:ok, path} ->
Logger.info("File saved to #{path}")
create_record(path)
{:error, :invalid_format} ->
{:error, "File format not supported"}
{:error, reason} ->
Logger.error("Upload failed: #{inspect(reason)}")
{:error, "Upload failed"}
end
end
Functions ending with ! raise errors instead of returning tuples.
# Returns {:ok, user} or {:error, changeset}
def create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
# Returns user or raises
def create_user!(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert!()
end
# Usage
try do
user = create_user!(invalid_attrs)
IO.puts("Created #{user.name}")
rescue
e in Ecto.InvalidChangesetError ->
IO.puts("Failed: #{inspect(e)}")
end
Catch exceptions when needed (use sparingly).
def parse_json(string) do
try do
{:ok, Jason.decode!(string)}
rescue
Jason.DecodeError -> {:error, :invalid_json}
end
end
Let processes fail and restart (preferred over defensive coding).
defmodule MyApp.Application do
use Application
def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
{MyApp.Worker, []}
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Handle errors in GenServer callbacks.
def handle_call(:risky_operation, _from, state) do
case perform_operation() do
{:ok, result} ->
{:reply, {:ok, result}, update_state(state, result)}
{:error, reason} ->
Logger.error("Operation failed: #{inspect(reason)}")
{:reply, {:error, reason}, state}
end
end
# Let it crash for unexpected errors
def handle_cast(:dangerous_work, state) do
# If this raises, supervisor will restart the process
result = dangerous_function!()
{:noreply, Map.put(state, :result, result)}
end
Return clear, actionable error messages.
def validate_image_upload(file) do
with :ok <- validate_file_type(file),
:ok <- validate_file_size(file),
:ok <- validate_dimensions(file) do
{:ok, file}
else
{:error, :invalid_type} ->
{:error, "Only JPEG, PNG, and GIF files are allowed"}
{:error, :too_large} ->
{:error, "File must be less than 10MB"}
{:error, :invalid_dimensions} ->
{:error, "Image must be at least 100x100 pixels"}
end
end
Extract and format Ecto changeset errors.
def changeset_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
# Usage
case create_user(attrs) do
{:ok, user} -> {:ok, user}
{:error, changeset} ->
errors = changeset_errors(changeset)
{:error, errors}
end
Use pattern matching in function heads for early returns.
def process_data(nil), do: {:error, :no_data}
def process_data([]), do: {:error, :empty_list}
def process_data(data) when is_list(data) do
# Process the list
{:ok, Enum.map(data, &transform/1)}
end
Don't check for things that can't happen. Let it crash.
Bad (defensive):
def get_username(user) do
if user && user.name do
user.name
else
"Unknown"
end
end
Good (trust your types):
def get_username(%User{name: name}), do: name
If the user is nil or doesn't have a name, it's a bug that should crash and be fixed.
Use @doc for public functions and @moduledoc for modules.
defmodule MyModule do
@moduledoc """
This module handles user operations.
"""
@doc """
Fetches a user by ID.
Returns `{:ok, user}` or `{:error, :not_found}`.
"""
def fetch_user(id), do: # ...
end
All data structures are immutable. Functions return new values rather than modifying in place.
# Always returns a new list
list = [1, 2, 3]
new_list = [0 | list] # [0, 1, 2, 3]
# list is still [1, 2, 3]
When writing test files for Elixir modules, invoke elixir-phoenix-guide:testing-essentials before writing any _test.exs file.
Use the capture operator & for concise anonymous functions.
Verbose:
Enum.map(list, fn x -> x * 2 end)
Concise:
Enum.map(list, &(&1 * 2))
Named function capture:
Enum.map(users, &User.format/1)