apply-phoenix-controller-conventions
Skill4 starsChanged 4 months agoMITView on GitHub
Use when writing new controller code in Phoenix applications. Enforces consistent patterns for RESTful routing, plug pipeline ordering, action methods, strong parameters, content negotiation, fallback controllers, and error handling. Covers resource routing, controller plugs, conn.assigns, json/html rendering, and authentication plugs. Trigger words: phoenix controller conventions, controller patterns, phoenix router, plug pipeline, controller plugs, fallback controller, strong params, phoenix routes, action fallback.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- ash-frameworkSkill
- benchee-profilingSkill
- broadway-data-pipelinesSkill
- cachex-cachingSkill
- code-qualitySkill
- code-reviewSkill
- deployment-gotchasSkill
- ecto-changeset-patternsSkill
- ecto-essentialsSkill
- ecto-migrationSkill
- ecto-nested-associationsSkill
- elixir-essentialsSkill
- gettext-i18nSkill
- liveview-streamsSkill
- mix-tasks-generatorsSkill
- oban-essentialsSkill
- otp-essentialsSkill
- phoenix-authenticationSkill
- phoenix-authorization-patternsSkill
- phoenix-channels-essentialsSkill
- phoenix-json-apiSkill
- phoenix-liveview-essentialsSkill
- phoenix-pubsub-patternsSkill
- phoenix-uploadsSkill
- property-based-testingSkill
- refactor-codeSkill
- req-http-clientSkill
- respond-to-reviewSkill
- security-essentialsSkill
- swoosh-emailsSkill
- telemetry-essentialsSkill
- testing-essentialsSkill
- typespec-dialyzerSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 22
Use this skill when writing new Phoenix controller modules or modifying existing controller code to ensure consistent, idiomatic patterns.
- Very specific: line 93
Checkpoint: Run mix phx.routes to verify routes resolve correctly and there are no unintended deep-nesting paths.
- Very specific: line 244
Checkpoint: Run mix test test/controllers/ after wiring up FallbackController to confirm each expected error tuple ({:error, :not_found}, …
- Very specific: line 265
Avoid get_user!/1 (raises) for user-triggered lookups.
- Distinctive: line 267
Checkpoint: Verify error paths in browser tests by asserting flash messages and redirect targets.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Apply Phoenix Controller Conventions
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.
Quick Reference
| Pattern | Convention |
| Routes | resources for RESTful; scope for grouping |
| Controllers | Thin — delegate business logic to contexts |
| Controller plugs | For auth, resource loading; halt or 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 |
RULES — Follow these with no exceptions
1. Keep controllers thin — never put business logic in controllers; delegate to context modules
2. Use plug guards for authentication and resource loading — chain with when action not in [...] opt-out pattern
3. Always validate and authorize every action that touches access-controlled resources
4. Use FallbackController for JSON API error handling — never inline catch-all case clauses in actions
5. Match content pipeline to format — API pipeline (no session, no CSRF) for JSON; browser pipeline for HTML
6. Use conn.assigns for passing data between plugs and actions — never use Process dictionaries
7. Never interpolate user input into redirect paths — use ~p"..." paths for verified routes
FCIS at this boundary
Controllers are edges: cast params, call context, render. No business math or multi-step domain orchestration in the action body.
❌ Bad: fat controller
def create(conn, %{"post" => params}) do
params = Map.put(params, "slug", slugify(params["title"]))
{:ok, post} = Repo.insert(Post.changeset(%Post{}, params))
redirect(conn, to: ~p"/posts/#{post}")
end✅ Good: thin action
def create(conn, %{"post" => params}) do
case Blog.create_post(conn.assigns.current_scope, params) do
{:ok, post} -> redirect(conn, to: ~p"/posts/#{post}")
{:error, changeset} -> render(conn, :new, form: to_form(changeset))
end
endRouting Conventions
✅ RESTful resources with explicit shallow-style split (Phoenix has no shallow: true):
scope "/", MyAppWeb do
pipe_through :browser
# Collection under parent
resources "/users", UserController do
resources "/posts", PostController, only: [:index, :new, :create]
end
# Member routes at top level
resources "/posts", PostController, only: [:show, :edit, :update, :delete]
endCheckpoint: Run mix phx.routes to verify routes resolve correctly and there are no unintended deep-nesting paths.
Plug Pipeline Ordering
✅ 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.
Action Patterns
✅ 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
endStrong Parameters / Params Validation
Validation 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()
endContent Negotiation
✅ 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.
FallbackController for JSON APIs
✅ 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.
Error Handling — Browser
✅ 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.
Common Pitfalls
| ❌ 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 |
Integration
| 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
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

