Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
73
91%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
Use this skill when writing new Phoenix controller modules or modifying existing controller code to ensure consistent, idiomatic patterns.
Precondition: Invoke phoenix-liveview-essentials before this skill if the feature uses LiveView; for traditional request/response, use this skill directly.
| Pattern | Convention |
|---|---|
| Routes | resources for RESTful; scope for grouping |
| Controllers | Thin — delegate business logic to contexts |
| before_action | For auth, resource loading; return conn |
| Strong params | Use changeset validation or cast/4 in context |
| Content type | Pipeline :browser for HTML; :api for JSON |
| Error handling | Use FallbackController for structured errors |
| Auth plugs | Include pipeline plugs; skip with :skip option |
before_action for authentication and resource loading — chain with :skip opt-out patternFallbackController for JSON API error handling — never inline error/2 or catch-all case clauses in actionsconn.assigns for passing data between plugs and actions — never use Process dictionaries~p"..." paths for verified routes❌ Bad — deep nesting, no scoping:
scope "/" do
get "/users/:user_id/posts/:post_id/comments", CommentController, :show
get "/users/:user_id/posts", PostController, :index
get "/users", UserController, :index
end✅ Good — RESTful resources with shallow nesting:
scope "/", MyAppWeb do
pipe_through :browser
resources "/users", UserController do
resources "/posts", PostController, only: [:index, :show], shallow: true
end
resources "/posts", PostController, only: [:index, :show]
endCheckpoint: Run mix phx.routes to verify routes resolve correctly.
❌ Bad — auth plug after action, before_action leaking across unrelated actions:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end
def edit(conn, %{"id" => id}) do
user = Accounts.get_user!(id)
render(conn, :edit, user: user)
end
def update(conn, %{"id" => id}) do
# No auth check...
end
end✅ Good — before_action with :skip opt-out, auth at controller level:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
plug :require_authenticated_user when action not in [:index, :show]
plug :load_user when action in [:edit, :update]
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end
def edit(conn, %{"id" => id}) do
user = conn.assigns.current_user
render(conn, :edit, user: user)
end
def update(conn, %{"id" => id}) do
user = conn.assigns.current_user
case Accounts.update_user(user, %{}) do
{:ok, user} -> redirect(conn, to: ~p"/users/#{user}")
{:error, changeset} -> render(conn, :edit, user: user, changeset: changeset)
end
end
defp require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> put_flash(:error, "You must be logged in")
|> redirect(to: ~p"/login")
|> halt()
end
end
defp load_user(conn, _opts) do
user = Accounts.get_user!(conn.params["id"])
assign(conn, :user, user)
end
end❌ Bad — business logic in controller, inline error handling, no fallback:
def create(conn, %{"user" => user_params}) do
changeset = User.changeset(%User{}, user_params)
case MyApp.Repo.insert(changeset) do
{:ok, user} ->
token = MyApp.Accounts.generate_token()
MyApp.Accounts.send_welcome_email(user.email, token)
conn
|> put_flash(:info, "User created")
|> redirect(to: ~p"/users/#{user}")
{:error, changeset} ->
render(conn, :new, changeset: changeset)
end
end✅ Good — thin controller, delegate to context, let it crash or use fallback:
def create(conn, %{"user" => user_params}) do
case Accounts.register_user(user_params) do
{:ok, user} ->
conn
|> put_flash(:info, "User created")
|> redirect(to: ~p"/users/#{user}")
{:error, changeset} ->
render(conn, :new, changeset: changeset)
end
endFor JSON API endpoints, use a FallbackController instead:
def create(conn, %{"user" => user_params}) do
with {:ok, user} <- Accounts.register_user(user_params) do
conn
|> put_status(:created)
|> render(:show, user: user)
end
end❌ Bad — mass assignment, no params validation:
def update(conn, %{"user" => user_params}) do
user = Accounts.get_user!(conn.params["id"])
Accounts.update_user(user, user_params) # User could send any field
end✅ Good — cast params in context or changeset:
def update(conn, %{"user" => user_params}) do
user = Accounts.get_user!(conn.params["id"])
case Accounts.update_user(user, user_params) do
{:ok, user} ->
redirect(conn, to: ~p"/users/#{user}")
{:error, changeset} ->
render(conn, :edit, changeset: changeset)
end
endThe context module validates fields:
def update_user(user, attrs) do
user
|> User.changeset(attrs) # cast/2 only permits expected fields
|> Repo.update()
end❌ Bad — browser pipeline for JSON endpoint, inline JSON generation:
# Router
scope "/api", MyAppWeb do
pipe_through :browser
resources "/users", Api.UserController
end
# Controller
def index(conn, _params) do
users = Accounts.list_users()
json(conn, %{data: users})
end✅ Good — API pipeline, proper rendering:
# Router
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", Api.UserController, only: [:index, :show]
end
# Controller
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end# Phoenix API pipeline (in router.ex)
pipeline :api do
plug :accepts, ["json"]
end❌ Bad — inline error handling in every action:
def show(conn, %{"id" => id}) do
case Accounts.get_user(id) do
{:ok, user} -> render(conn, :show, user: user)
{:error, :not_found} -> put_status(conn, :not_found) |> json(%{error: "Not found"})
{:error, _} -> put_status(conn, :internal_server_error) |> json(%{error: "Server error"})
end
end✅ Good — action_fallback + FallbackController:
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
action_fallback MyAppWeb.FallbackController
def show(conn, %{"id" => id}) do
with {:ok, user} <- Accounts.get_user(id) do
render(conn, :show, user: user)
end
end
end
defmodule MyAppWeb.FallbackController do
use MyAppWeb, :controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(%{error: "Not found"})
end
def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:forbidden)
|> json(%{error: "Forbidden"})
end
end❌ Bad — raising on expected errors:
def show(conn, %{"id" => id}) do
user = Accounts.get_user!(id) # Raises if not found
render(conn, :show, user: user)
end✅ Good — pattern match and render:
def show(conn, %{"id" => id}) do
case Accounts.get_user(id) do
{:ok, user} ->
render(conn, :show, user: user)
{:error, :not_found} ->
conn
|> put_flash(:error, "User not found")
|> redirect(to: ~p"/users")
|> halt()
end
end| ❌ Wrong | ✅ Correct |
|---|---|
Business logic in controller (e.g., Repo.insert inline) | Delegate to context module (Accounts.create_user) |
before_action without :skip on login/exempt actions | Use plug :auth when action not in [:index, :show] |
redirect(to: user_provided_url) | Use ~p"..." verified path or url helpers |
| JSON error handling in each action | Use action_fallback FallbackController |
json(conn, ...) in browser pipeline | Use pipe_through :api for JSON endpoints |
| Process dictionaries for passing data between plugs | Use conn.assigns |
| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | apply-phoenix-controller-conventions | code-quality |
| phoenix-json-api | apply-phoenix-controller-conventions | testing-essentials |
Companion skills:
phoenix-json-api — RESTful API controller patterns and versioningphoenix-liveview-essentials — LiveView for interactive pagesphoenix-scopes — authentication and authorization setupphoenix-uploads — file upload in controller actions