Guide for building Phoenix web applications with LiveView, contexts, channels, and following Phoenix best practices
Provides guidance for building Phoenix apps with LiveView, contexts, and channels. Activates when you create/modify Phoenix projects, implement real-time features, or structure web applications following Phoenix conventions.
/plugin marketplace add vinnie357/claude-skills/plugin install all-skills@vinnie357This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill activates when working with Phoenix web applications, including setup, development, LiveView, contexts, controllers, and channels.
Activate this skill when:
Follow Phoenix conventions:
lib/
my_app/ # Business logic and contexts
accounts/ # Domain contexts
repo.ex
my_app_web/ # Web interface
controllers/
live/ # LiveView modules
components/ # Function components
router.ex
endpoint.ex
Organize business logic into contexts (bounded domains):
Generate contexts with related schemas:
mix phx.gen.context Accounts User users email:string name:string
Structure contexts to encapsulate business logic:
defmodule MyApp.Accounts do
@moduledoc """
The Accounts context - manages user accounts and authentication.
"""
alias MyApp.Repo
alias MyApp.Accounts.User
def list_users do
Repo.all(User)
end
def get_user!(id), do: Repo.get!(User, id)
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def update_user(%User{} = user, attrs) do
user
|> User.changeset(attrs)
|> Repo.update()
end
end
LiveView enables rich, real-time experiences without writing JavaScript.
Understand the mount → handle_event → render cycle:
defmodule MyAppWeb.UserLive.Index do
use MyAppWeb, :live_view
alias MyApp.Accounts
@impl true
def mount(_params, _session, socket) do
# Runs on initial page load and live connection
{:ok, assign(socket, :users, list_users())}
end
@impl true
def handle_params(params, _url, socket) do
# Runs after mount and on live patch
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
user = Accounts.get_user!(id)
{:ok, _} = Accounts.delete_user(user)
{:noreply, assign(socket, :users, list_users())}
end
@impl true
def render(assigns) do
~H"""
<div>
<.table rows={@users} id="users">
<:col :let={user} label="Name"><%= user.name %></:col>
<:col :let={user} label="Email"><%= user.email %></:col>
<:action :let={user}>
<.button phx-click="delete" phx-value-id={user.id}>Delete</.button>
</:action>
</.table>
</div>
"""
end
defp list_users do
Accounts.list_users()
end
end
mount/3 for initial data loadinghandle_params/3assign_new/3 for expensive computationsphx-debounce and phx-throttle for frequent eventsCreate reusable components:
defmodule MyAppWeb.Components.UserCard do
use Phoenix.Component
attr :user, :map, required: true
attr :class, :string, default: ""
def user_card(assigns) do
~H"""
<div class={"card " <> @class}>
<h3><%= @user.name %></h3>
<p><%= @user.email %></p>
</div>
"""
end
end
Use with <.user_card user={@current_user} /> in templates.
Use changesets for validation:
@impl true
def mount(_params, _session, socket) do
changeset = Accounts.change_user(%User{})
{:ok, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("validate", %{"user" => user_params}, socket) do
changeset =
%User{}
|> Accounts.change_user(user_params)
|> Map.put(:action, :validate)
{:noreply, assign(socket, form: to_form(changeset))}
end
@impl true
def handle_event("save", %{"user" => user_params}, socket) do
case Accounts.create_user(user_params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created successfully")
|> push_navigate(to: ~p"/users/#{user}")}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
def render(assigns) do
~H"""
<.form for={@form} phx-change="validate" phx-submit="save">
<.input field={@form[:name]} label="Name" />
<.input field={@form[:email]} label="Email" type="email" />
<.button>Save</.button>
</.form>
"""
end
Structure routes logically:
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/", HomeLive, :index
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id", UserLive.Show, :show
end
scope "/api", MyAppWeb do
pipe_through :api
resources "/users", UserController, except: [:new, :edit]
end
end
Use live actions for modal/overlay states:
live "/users", UserLive.Index, :index
live "/users/new", UserLive.Index, :new
live "/users/:id/edit", UserLive.Index, :edit
Then handle in handle_params/3:
defp apply_action(socket, :edit, %{"id" => id}) do
socket
|> assign(:page_title, "Edit User")
|> assign(:user, Accounts.get_user!(id))
end
defp apply_action(socket, :new, _params) do
socket
|> assign(:page_title, "New User")
|> assign(:user, %User{})
end
defp apply_action(socket, :index, _params) do
socket
|> assign(:page_title, "Listing Users")
|> assign(:user, nil)
end
For custom real-time protocols:
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
@impl true
def join("room:" <> room_id, _payload, socket) do
if authorized?(socket, room_id) do
{:ok, assign(socket, :room_id, room_id)}
else
{:error, %{reason: "unauthorized"}}
end
end
@impl true
def handle_in("new_msg", %{"body" => body}, socket) do
broadcast!(socket, "new_msg", %{body: body, user: socket.assigns.user})
{:noreply, socket}
end
end
For LiveView updates and process communication:
# Subscribe in mount
def mount(_params, _session, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(MyApp.PubSub, "users")
end
{:ok, assign(socket, :users, list_users())}
end
# Handle broadcasts
def handle_info({:user_created, user}, socket) do
{:noreply, update(socket, :users, fn users -> [user | users] end)}
end
# Broadcast from context
def create_user(attrs) do
with {:ok, user} <- do_create_user(attrs) do
Phoenix.PubSub.broadcast(MyApp.PubSub, "users", {:user_created, user})
{:ok, user}
end
end
defmodule MyAppWeb.UserControllerTest do
use MyAppWeb.ConnCase, async: true
test "GET /users", %{conn: conn} do
conn = get(conn, ~p"/users")
assert html_response(conn, 200) =~ "Listing Users"
end
end
defmodule MyAppWeb.UserLiveTest do
use MyAppWeb.ConnCase
import Phoenix.LiveViewTest
test "displays users", %{conn: conn} do
user = insert(:user)
{:ok, view, html} = live(conn, ~p"/users")
assert html =~ user.name
assert has_element?(view, "#user-#{user.id}")
end
test "creates user", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/users/new")
assert view
|> form("#user-form", user: %{name: "Alice", email: "alice@example.com"})
|> render_submit()
assert_patch(view, ~p"/users")
end
end
defmodule MyAppWeb.RoomChannelTest do
use MyAppWeb.ChannelCase
test "broadcasts are pushed to the client", %{socket: socket} do
{:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{})
broadcast_from!(socket, "new_msg", %{body: "test"})
assert_broadcast "new_msg", %{body: "test"}
end
end
Preload associations efficiently:
def list_posts do
Post
|> preload([:author, comments: :author])
|> Repo.all()
end
Use Scrivener or custom pagination:
def list_users(page \\ 1) do
User
|> order_by(desc: :inserted_at)
|> Repo.paginate(page: page, page_size: 20)
end
Handle uploads in LiveView:
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:uploaded_files, [])
|> allow_upload(:avatar, accept: ~w(.jpg .jpeg .png), max_entries: 1)}
end
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, "/uploads/" <> Path.basename(dest)}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end
preload/2 to avoid N+1 queriesselect/3 to load only needed fieldsRepo.stream/2 for large datasetshandle_event or background jobsassign_new/3 for computed valueshandle_continue/2 for async operations after mountassign(socket, :items, temporary: true)Use Cachex or ETS for caching:
def get_user!(id) do
Cachex.fetch(:users, id, fn ->
{:commit, Repo.get!(User, id)}
end)
end
put_secure_browser_headers plugThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.