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).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
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 |
plug guards for authentication and resource loading — chain with when action not in [...] opt-out patternFallbackController for JSON API error handling — never inline catch-all case clauses in actionsconn.assigns for passing data between plugs and actions — never use Process dictionaries~p"..." paths for verified routes✅ 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 and there are no unintended deep-nesting paths.
✅ plug guards with 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, _params) do
render(conn, :edit, user: conn.assigns.user)
end
def update(conn, %{"user" => user_params}) do
case Accounts.update_user(conn.assigns.user, user_params) do
{:ok, user} -> redirect(conn, to: ~p"/users/#{user}")
{:error, changeset} -> render(conn, :edit, user: conn.assigns.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
endCheckpoint: Confirm plug ordering with mix phx.routes and verify that exempt actions (e.g., :index, :show) do not trigger auth plugs in integration tests.
✅ Thin controller delegating to context:
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 with + FallbackController instead of case:
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
endValidation belongs in the context, not the controller. The controller passes params through unchanged (see the update/2 example in Plug Pipeline Ordering), while the context enforces permitted fields:
# Context — enforce permitted fields via changeset
def update_user(user, attrs) do
user
|> User.changeset(attrs) # cast/2 only permits declared fields
|> Repo.update()
end✅ API pipeline for JSON, browser pipeline for HTML:
# Router
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", Api.UserController, only: [:index, :show]
end
# Phoenix API pipeline (router.ex)
pipeline :api do
plug :accepts, ["json"]
end
# Controller — use render/3, not json/2, so views handle serialisation
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
endCheckpoint: Confirm the correct pipeline is applied by inspecting mix phx.routes output and checking that API routes lack :fetch_session and :protect_from_forgery plugs.
✅ action_fallback + centralised 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
endCheckpoint: Run mix test test/controllers/ after wiring up FallbackController to confirm each expected error tuple ({:error, :not_found}, {:error, :unauthorized}) is matched and returns the correct HTTP status.
✅ Pattern match on expected errors; redirect with flash:
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
endAvoid get_user!/1 (raises) for user-triggered lookups; reserve bang variants for developer errors where a crash is the correct signal.
Checkpoint: Verify error paths in browser tests by asserting flash messages and redirect targets.
| ❌ Wrong | ✅ Correct |
|---|---|
Business logic in controller (Repo.insert inline) | Delegate to context module (Accounts.create_user) |
| Auth plug without action guard on public actions | Use plug :auth when action not in [:index, :show] |
redirect(to: user_provided_url) | Use ~p"..." verified path helpers |
| JSON error handling duplicated in each action | Use action_fallback FallbackController |
pipe_through :browser for JSON endpoints | Use pipe_through :api for JSON scopes |
Process dictionary for inter-plug data | 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.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators