Skills phoenix
install
source · Clone the upstream repo
git clone https://github.com/TerminalSkills/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/TerminalSkills/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/phoenix" ~/.claude/skills/terminalskills-skills-phoenix && rm -rf "$T"
manifest:
skills/phoenix/SKILL.mdsource content
Phoenix
Phoenix leverages the Erlang VM (BEAM) for massive concurrency and fault tolerance. LiveView enables rich, real-time UIs with server-rendered HTML — no JavaScript framework needed.
Installation
# Install Phoenix and create project mix archive.install hex phx_new mix phx.new my_app --database postgres cd my_app mix setup # deps.get + ecto.create + assets
Project Structure
# Phoenix project layout lib/my_app/ ├── application.ex # OTP supervision tree ├── repo.ex # Ecto repo ├── accounts/ # Context module │ ├── user.ex # Ecto schema │ └── accounts.ex # Business logic lib/my_app_web/ ├── endpoint.ex # HTTP entry point ├── router.ex # Routes ├── controllers/ # REST controllers ├── live/ # LiveView modules ├── components/ # Function components └── templates/ # HEEx templates
Ecto Schema and Migration
# lib/my_app/articles/article.ex — Ecto schema defmodule MyApp.Articles.Article do use Ecto.Schema import Ecto.Changeset schema "articles" do field :title, :string field :slug, :string field :body, :string field :published, :boolean, default: false belongs_to :author, MyApp.Accounts.User timestamps() end def changeset(article, attrs) do article |> cast(attrs, [:title, :body, :published]) |> validate_required([:title, :body]) |> validate_length(:title, max: 200) |> unique_constraint(:slug) |> generate_slug() end defp generate_slug(changeset) do case get_change(changeset, :title) do nil -> changeset title -> put_change(changeset, :slug, Slug.slugify(title)) end end end
# priv/repo/migrations/20240101000000_create_articles.exs — migration defmodule MyApp.Repo.Migrations.CreateArticles do use Ecto.Migration def change do create table(:articles) do add :title, :string, null: false, size: 200 add :slug, :string, null: false add :body, :text, null: false add :published, :boolean, default: false add :author_id, references(:users, on_delete: :delete_all), null: false timestamps() end create unique_index(:articles, [:slug]) end end
Context Module
# lib/my_app/articles/articles.ex — business logic context defmodule MyApp.Articles do import Ecto.Query alias MyApp.Repo alias MyApp.Articles.Article def list_published do Article |> where(published: true) |> order_by(desc: :inserted_at) |> preload(:author) |> Repo.all() end def get_article!(id), do: Repo.get!(Article, id) |> Repo.preload(:author) def create_article(attrs) do %Article{} |> Article.changeset(attrs) |> Repo.insert() end def update_article(%Article{} = article, attrs) do article |> Article.changeset(attrs) |> Repo.update() end end
Router
# lib/my_app_web/router.ex — routing 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 "/articles", ArticleLive.Index, :index live "/articles/:slug", ArticleLive.Show, :show end scope "/api", MyAppWeb.API do pipe_through :api resources "/articles", ArticleController, only: [:index, :show, :create] end end
LiveView
# lib/my_app_web/live/article_live/index.ex — LiveView for article list defmodule MyAppWeb.ArticleLive.Index do use MyAppWeb, :live_view alias MyApp.Articles @impl true def mount(_params, _session, socket) do articles = Articles.list_published() {:ok, assign(socket, articles: articles, search: "")} end @impl true def handle_event("search", %{"query" => query}, socket) do articles = Articles.search(query) {:noreply, assign(socket, articles: articles, search: query)} end end
<!-- lib/my_app_web/live/article_live/index.html.heex — LiveView template --> <div> <h1>Articles</h1> <form phx-change="search" phx-debounce="300"> <input type="text" name="query" value={@search} placeholder="Search..." /> </form> <div :for={article <- @articles}> <h2><.link navigate={~p"/articles/#{article.slug}"}><%= article.title %></.link></h2> <p>By <%= article.author.name %></p> </div> </div>
JSON Controller
# lib/my_app_web/controllers/api/article_controller.ex — API controller defmodule MyAppWeb.API.ArticleController do use MyAppWeb, :controller alias MyApp.Articles def index(conn, _params) do articles = Articles.list_published() json(conn, %{data: Enum.map(articles, &article_json/1)}) end def create(conn, %{"article" => params}) do case Articles.create_article(params) do {:ok, article} -> conn |> put_status(:created) |> json(%{data: article_json(article)}) {:error, changeset} -> conn |> put_status(422) |> json(%{errors: format_errors(changeset)}) end end defp article_json(a), do: %{id: a.id, title: a.title, slug: a.slug} end
Channels (Real-time)
# lib/my_app_web/channels/room_channel.ex — WebSocket channel defmodule MyAppWeb.RoomChannel do use MyAppWeb, :channel def join("room:" <> room_id, _params, socket) do {:ok, assign(socket, :room_id, room_id)} end def handle_in("message", %{"body" => body}, socket) do broadcast!(socket, "message", %{body: body, user: socket.assigns.user_id}) {:noreply, socket} end end
Testing
# test/my_app/articles_test.exs — context test defmodule MyApp.ArticlesTest do use MyApp.DataCase alias MyApp.Articles test "create_article/1 with valid data" do attrs = %{title: "Test", body: "Content", author_id: user_fixture().id} assert {:ok, article} = Articles.create_article(attrs) assert article.title == "Test" end end
Key Commands
# Common Mix commands mix phx.gen.live Articles Article articles title:string body:text mix ecto.migrate mix phx.routes # Show all routes mix test mix phx.server # Start dev server iex -S mix phx.server # Start with REPL
Key Patterns
- Organize business logic in Context modules — controllers/LiveViews delegate to contexts
- Use changesets for all data validation — they compose and return descriptive errors
- Use
explicitly — Ecto never lazy-loads to prevent N+1preload - LiveView
replaces most JavaScript interactivityhandle_event - Use PubSub (
) for real-time updates across LiveView processesPhoenix.PubSub.broadcast - Use
for transactions spanning multiple operationsEcto.Multi