Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
73
91%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Advisory
Suggest reviewing before use
:api pipeline — don't mix HTML and JSON pipelines; API routes skip CSRF and sessions{:error, changeset} must become {"errors": {...}}/api/v1/) — not headers; URL versioning is visible and cacheableFallbackController for consistent error handling — every action returns {:ok, result} or {:error, reason}Follow these steps in order when constructing a new API endpoint:
:api (or :api_auth) pipeline scope with a versioned URL prefixaction_fallback MyAppWeb.FallbackController and return {:ok, _} / {:error, _} from every action{"errors": {...}}) before proceedingApiAuth) to protected scopes; confirm that missing/invalid tokens yield 401 with a JSON bodyindex accepts page/per_page params and never returns an unbounded collectiondefmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
pipeline :api_auth do
plug MyAppWeb.Plugs.ApiAuth
end
# Public endpoints
scope "/api/v1", MyAppWeb.API.V1, as: :api_v1 do
pipe_through :api
post "/auth/login", AuthController, :login
post "/auth/register", AuthController, :register
end
# Protected endpoints
scope "/api/v1", MyAppWeb.API.V1, as: :api_v1 do
pipe_through [:api, :api_auth]
resources "/posts", PostController, except: [:new, :edit]
end
enddefmodule MyAppWeb.API.V1.PostController do
use MyAppWeb, :controller
alias MyApp.Blog
alias MyApp.Blog.Post
action_fallback MyAppWeb.FallbackController
def index(conn, params) do
page = Map.get(params, "page", "1") |> String.to_integer()
per_page = Map.get(params, "per_page", "20") |> String.to_integer() |> min(100)
{posts, total} = Blog.list_posts(page: page, per_page: per_page)
conn
|> put_resp_header("x-total-count", to_string(total))
|> json(%{
data: Enum.map(posts, &post_json/1),
meta: %{page: page, per_page: per_page, total: total}
})
end
def create(conn, %{"post" => post_params}) do
with {:ok, %Post{} = post} <- Blog.create_post(post_params) do
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/api/v1/posts/#{post}")
|> json(%{data: post_json(post)})
end
end
defp post_json(post) do
%{
id: post.id,
title: post.title,
body: post.body,
inserted_at: post.inserted_at
}
end
enddefmodule MyAppWeb.FallbackController do
use MyAppWeb, :controller
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> json(%{errors: %{detail: "Resource not found"}})
end
def call(conn, {:error, :unauthorized}) do
conn
|> put_status(:unauthorized)
|> json(%{errors: %{detail: "Not authorized"}})
end
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: format_changeset_errors(changeset)})
end
defp format_changeset_errors(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
enddefmodule MyAppWeb.Plugs.ApiAuth do
import Plug.Conn
import Phoenix.Controller
def init(opts), do: opts
def call(conn, _opts) do
with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
{:ok, user} <- MyApp.Accounts.get_user_by_api_token(token) do
assign(conn, :current_user, user)
else
_ ->
conn
|> put_status(:unauthorized)
|> json(%{errors: %{detail: "Invalid or missing token"}})
|> halt()
end
end
end| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | phoenix-json-api | testing-essentials |
| security-essentials | phoenix-json-api | req-http-client |