toolchains/elixir/frameworks/phoenix-api-channels/SKILL.md
Phoenix controllers, JSON APIs, Channels, and Presence on the BEAM
npx skillsauth add bobmatnyc/claude-mpm-skills phoenix-api-channelsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Phoenix excels at REST/JSON APIs and WebSocket Channels with minimal boilerplate, leveraging the BEAM for fault tolerance, lightweight processes, and supervised PubSub/Presence.
Core pillars
mix phx.new my_api --no-html --no-live
cd my_api
mix deps.get
mix ecto.create
mix phx.server
Key files:
lib/my_api_web/endpoint.ex — plugs, sockets, instrumentationlib/my_api_web/router.ex — pipelines, scopes, versioning, socketslib/my_api_web/controllers/* — REST/JSON controllerslib/my_api/* — contexts + Ecto schemas (ownership of data logic)lib/my_api_web/channels/* — Channel modulesSeparate browser vs API pipelines; version APIs with scopes.
defmodule MyApiWeb.Router do
use MyApiWeb, :router
pipeline :api do
plug :accepts, ["json"]
plug :fetch_session
plug :protect_from_forgery
plug MyApiWeb.Plugs.RequireAuth
end
scope "/api", MyApiWeb do
pipe_through :api
scope "/v1", V1, as: :v1 do
resources "/users", UserController, except: [:new, :edit]
post "/sessions", SessionController, :create
end
end
socket "/socket", MyApiWeb.UserSocket,
websocket: [connect_info: [:peer_data, :x_headers]],
longpoll: false
end
Tips
socket "/socket" for Channels; restrict transports as needed.Controllers stay thin; contexts own the logic.
defmodule MyApiWeb.V1.UserController do
use MyApiWeb, :controller
alias MyApi.Accounts
action_fallback MyApiWeb.FallbackController
def index(conn, _params) do
users = Accounts.list_users()
render(conn, :index, users: users)
end
def create(conn, params) do
with {:ok, user} <- Accounts.register_user(params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p\"/api/v1/users/#{user.id}\")
|> render(:show, user: user)
end
end
end
FallbackController centralizes error translation ({:error, :not_found} → 404 JSON).
Plugs
RequireAuth verifies bearer/session tokens, sets current_user.plug :scrub_params-style transforms in pipelines, not controllers.Contexts expose only what controllers/channels need.
defmodule MyApi.Accounts do
import Ecto.Query, warn: false
alias MyApi.{Repo, Accounts.User}
def list_users, do: Repo.all(User)
def get_user!(id), do: Repo.get!(User, id)
def register_user(attrs) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
end
Guidelines
Ecto.Multi for multi-step operations.Scrivener, Flop) for large lists.Channel module example:
defmodule MyApiWeb.RoomChannel do
use Phoenix.Channel
alias Phoenix.Presence
def join("room:" <> room_id, _payload, socket) do
send(self(), :after_join)
{:ok, assign(socket, :room_id, room_id)}
end
def handle_info(:after_join, socket) do
Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)})
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
def handle_in("message:new", %{"body" => body}, socket) do
broadcast!(socket, "message:new", %{user_id: socket.assigns.user_id, body: body})
{:noreply, socket}
end
end
PubSub from contexts
def create_order(attrs) do
with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
Phoenix.PubSub.broadcast(MyApi.PubSub, "orders", {:order_created, order})
{:ok, order}
end
end
Best practices
UserSocket.connect/3 before joining topics."tenant:" <> tenant_id <> ":room:" <> room_id).authorization: Bearer <token>; verify in plug, assign current_user.Phoenix.Token.sign/verify for short-lived join params.Endpoint with cors_plug.Use generated helpers:
defmodule MyApiWeb.UserControllerTest do
use MyApiWeb.ConnCase, async: true
test "lists users", %{conn: conn} do
conn = get(conn, ~p\"/api/v1/users\")
assert json_response(conn, 200)["data"] == []
end
end
Channel tests:
defmodule MyApiWeb.RoomChannelTest do
use MyApiWeb.ChannelCase, async: true
test "broadcasts messages" do
{:ok, _, socket} = connect(MyApiWeb.UserSocket, %{"token" => "abc"})
{:ok, _, socket} = subscribe_and_join(socket, "room:123", %{})
ref = push(socket, "message:new", %{"body" => "hi"})
assert_reply ref, :ok
assert_broadcast "message:new", %{body: "hi"}
end
end
DataCase: isolates DB per test; use fixtures/factories for setup.
:telemetry events from endpoint, controller, channel, and Ecto queries; export via OpentelemetryPhoenix and OpentelemetryEcto.Plug.Telemetry for request metrics; add logging metadata (request_id, user_id).MIX_ENV=prod mix release; configure runtime in config/runtime.exs.libcluster + distributed PubSub for multi-node Presence.UserSocket.connect/3, leading to topic exposure.action_fallback → inconsistent error shapes.Phoenix API + Channels shine when contexts own data, controllers stay thin, and Channels use PubSub/Presence with strict authorization and telemetry. The BEAM handles concurrency and fault tolerance; focus on clear boundaries and real-time experiences.
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
development
Visual verification workflow for UI changes to accelerate code review and catch ...