使い方
- インストールコマンドをコピーし、自分の環境へ導入する前にソースを確認します。
- 必要なツール、認証情報、対応環境を原文で確認します。
- 入力と成果物を指定し、出力を資料と照合します。
npx skills add https://github.com/TerminalSkills/skills --skill phoenix開始前の確認
- 導入にはモデル契約、API枠、ファイルへのアクセス許可は含まれません。
- スター数や導入数は活動の指標であり、独立した品質・安全性試験ではありません。
見出し、例、コード、表、リンク、参照画像を含む原文を表示しています。
Phoenix
Overview
Phoenix leverages the Erlang VM (BEAM) for massive concurrency and fault tolerance. LiveView enables rich, real-time UIs with server-rendered HTML, with little custom JavaScript. This skill targets Phoenix 1.8 (1.8.16 checked) with LiveView 1.x and Ecto 3.
Instructions
Installation
mix archive.install hex phx_new # asks to confirm; add --force to skip the prompt
mix phx.new blog --database postgres # PostgreSQL is the default
cd blog
mix setup # deps.get, ecto.setup, assets.setup, assets.build
mix phx.server # http://localhost:4000 (dev server binds to localhost)On Linux install inotify-tools for live reload. The examples below use the app name my_app; substitute your own.
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, :author_id])
|> validate_required([:title, :body, :author_id])
|> 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, slugify(title))
end
end
defp slugify(title) do
title |> String.downcase() |> String.replace(~r/[^a-z0-9]+/u, "-") |> String.trim("-")
end
end# priv/repo/migrations/20261024120000_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
endContext Module
# lib/my_app/articles.ex
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_published_by_slug!(slug) do
Article |> where(slug: ^slug, published: true) |> preload(:author) |> Repo.one!()
end
def search(term) do
Article
|> where([a], a.published and ilike(a.title, ^"%#{term}%"))
|> order_by(desc: :inserted_at)
|> preload(:author)
|> Repo.all()
end
def create_article(attrs) do
%Article{}
|> Article.changeset(attrs)
|> Repo.insert()
end
endRouter
# lib/my_app_web/router.ex — routing
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# pipeline :browser (session, flash, root layout, CSRF) is generated by mix phx.new
pipeline :api do
plug :accepts, ["json"]
end
scope "/", MyAppWeb do
pipe_through :browser
live "/articles", ArticleLive.Index, :index
live "/articles/:slug", ArticleLive.Show, :show # Show: see mix phx.gen.live
end
scope "/api", MyAppWeb.API do
pipe_through :api
resources "/articles", ArticleController, only: [:index, :create]
end
endLiveView
# lib/my_app_web/live/article_live/index.ex
defmodule MyAppWeb.ArticleLive.Index do
use MyAppWeb, :live_view
alias MyApp.Articles
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, articles: Articles.list_published(), search: "")}
end
@impl true
def handle_event("search", %{"query" => query}, socket) do
{:noreply, assign(socket, articles: Articles.search(query), search: query)}
end
end<div>
<h1>Articles</h1>
<form phx-change="search">
<input type="text" name="query" value={@search} phx-debounce="300" placeholder="Search..." />
</form>
<div :for={article <- @articles}>
<h2><.link navigate={~p"/articles/#{article.slug}"}>{article.title}</.link></h2>
<p>By {article.author.email}</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
json(conn, %{data: Enum.map(Articles.list_published(), &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}
defp format_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, _opts} -> msg end)
end
endChannels (Real-time)
# lib/my_app_web/channels/user_socket.ex — authenticate before any channel joins
defmodule MyAppWeb.UserSocket do
use Phoenix.Socket
channel "room:*", MyAppWeb.RoomChannel
@impl true
def connect(%{"token" => token}, socket, _connect_info) do
case Phoenix.Token.verify(MyAppWeb.Endpoint, "user socket", token, max_age: 86_400) do
{:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)}
{:error, _} -> :error
end
end
def connect(_params, _socket, _connect_info), do: :error
@impl true
def id(socket), do: "user_socket:#{socket.assigns.user_id}"
end
# lib/my_app_web/channels/room_channel.ex
defmodule MyAppWeb.RoomChannel do
use MyAppWeb, :channel
def join("room:" <> _room_id, _params, socket), do: {:ok, socket}
def handle_in("message", %{"body" => body}, socket) do
broadcast!(socket, "message", %{body: body, user: socket.assigns.user_id})
{:noreply, socket}
end
endRegister the socket in endpoint.ex: socket "/socket", MyAppWeb.UserSocket, websocket: true, longpoll: false.
Testing
defmodule MyApp.ArticlesTest do
use MyApp.DataCase
import MyApp.AccountsFixtures # created by `mix phx.gen.auth Accounts User users`
test "create_article/1 builds a slug" do
attrs = %{title: "Launch notes", body: "Content", author_id: user_fixture().id}
assert {:ok, article} = MyApp.Articles.create_article(attrs)
assert article.slug == "launch-notes"
end
endKey Commands
mix phx.gen.live Articles Article articles title:string body:text generates a LiveView CRUD with migration and tests; mix ecto.migrate, mix phx.routes, mix test, iex -S mix phx.server (dev server with REPL), mix phx.gen.secret (value for SECRET_KEY_BASE).
Examples
Example 1: "Add a blog section with search"
Run mix phx.gen.auth Accounts User users for authors, add the schema, migration and context above, run mix ecto.migrate, and add the live routes. Typing in the search box sends a search event (debounced 300 ms) and the list re-renders without a page load.
Example 2: "Give me a JSON endpoint for mobile"
Add the /api scope and ArticleController. curl -s -X POST localhost:4000/api/articles -H 'content-type: application/json' -d '{"article":{"title":"Launch notes","body":"Hello","author_id":1}}' returns 201 with {"data":{"id":1,"title":"Launch notes","slug":"launch-notes"}}; a missing title returns 422 with {"errors":{"title":["can't be blank"]}}.
Guidelines
- Production config is read from
config/runtime.exs: setSECRET_KEY_BASE,DATABASE_URL,PHX_HOSTandPHX_SERVER=trueas environment variables; build withMIX_ENV=prod mix release. - Authenticate sockets and check authorization in every
handle_event/handle_in; clients can send any event name and payload. - Pass only whitelisted fields to
cast/3(nevercast(attrs, Map.keys(attrs))) and never cast privileged fields such asis_admin. - Keep the
:browserpipeline's CSRF protection; skip it only in:apipipelines that authenticate with tokens. - Use
phx-debounceon inputs that fire on every keystroke, andstream/3instead of assigning large lists to the socket.
Key Patterns
- Business logic lives in context modules; controllers and LiveViews only delegate to them.
- Validate through changesets; Ecto never lazy-loads, so
preloadexplicitly. - Use
Ecto.Multifor multi-step transactions andPhoenix.PubSub.broadcastto push updates across LiveView processes.

