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
cast_assoc/3 for has_many/has_one — never manually insert children in a separate stepEcto.Multi for operations spanning multiple unrelated tables — do NOT use Ecto.Multi for nested associationson_delete explicitly in migrations — :delete_all for owned children, :nothing for independent entitieson_replace: :delete in cast_assoc for list management — allows removing items by omitting themcast_assoc compares against currently loaded datacast_assoc sets them automaticallyRepo.transaction/1 with Ecto.Multi — wrap multi-table operations for atomicityFollow this sequence for ANY nested association or multi-table operation:
has_many/belongs_to with appropriate on_replace strategyon_delete and create indexcast_assocRepo.insert/Repo.update with parent changeset{:ok, _} and {:error, changeset}defmodule MyApp.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
has_many :comments, MyApp.Blog.Comment
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> validate_required([:title])
|> cast_assoc(:comments, with: &MyApp.Blog.Comment.changeset/2)
end
end
defmodule MyApp.Blog.Comment do
use Ecto.Schema
import Ecto.Changeset
schema "comments" do
field :body, :string
belongs_to :post, MyApp.Blog.Post
timestamps()
end
def changeset(comment, attrs) do
comment
|> cast(attrs, [:body])
|> validate_required([:body])
end
end
# Usage — create post with comments in one operation
Blog.create_post(%{
title: "My Post",
comments: [
%{body: "First comment"},
%{body: "Second comment"}
]
})Nested changeset errors are embedded inside the parent changeset. Pattern-match on both outcomes:
case Repo.insert(Post.changeset(%Post{}, attrs)) do
{:ok, post} ->
{:ok, post}
{:error, changeset} ->
# Top-level errors on changeset.errors
# Nested errors on changeset.changes[:comments] (list of changesets)
{:error, changeset}
enddefmodule MyApp.Recipes.Recipe do
schema "recipes" do
field :name, :string
has_many :ingredients, MyApp.Recipes.Ingredient, on_replace: :delete
timestamps()
end
def changeset(recipe, attrs) do
recipe
|> cast(attrs, [:name])
|> validate_required([:name])
|> cast_assoc(:ingredients, with: &MyApp.Recipes.Ingredient.changeset/2)
end
end
# Update — send the full list; omitted items are deleted
def update_recipe(recipe, attrs) do
recipe
|> Repo.preload(:ingredients)
|> Recipe.changeset(attrs)
|> Repo.update()
enddef create_order_with_payment(order_attrs, payment_attrs) do
Ecto.Multi.new()
|> Ecto.Multi.insert(:order, Order.changeset(%Order{}, order_attrs))
|> Ecto.Multi.insert(:payment, fn %{order: order} ->
Payment.changeset(%Payment{}, Map.put(payment_attrs, :order_id, order.id))
end)
|> Repo.transaction()
endAlways pattern-match on both success and error tuples from Repo.transaction/1:
case create_order_with_payment(order_attrs, payment_attrs) do
{:ok, %{order: order, payment: payment}} ->
{:ok, order}
{:error, failed_operation, failed_changeset, _changes_so_far} ->
Logger.error("Multi failed at #{failed_operation}: #{inspect(failed_changeset.errors)}")
{:error, failed_changeset}
enddefmodule MyApp.Repo.Migrations.CreateComments do
use Ecto.Migration
def change do
create table(:comments) do
add :body, :text
add :post_id, references(:posts, on_delete: :delete_all)
timestamps()
end
create index(:comments, [:post_id])
end
endConfirm FK indexes exist in psql with \d comments. Expect an entry such as comments_post_id_index. If missing, add it in a new migration:
def change do
create index(:comments, [:post_id])
endUse a join schema with cast_assoc for full control over nested creation and updates:
# Schema
schema "posts" do
field :title, :string
many_to_many :tags, MyApp.Blog.Tag, join_through: MyApp.Blog.PostTag, on_replace: :delete
timestamps()
end
# Join schema
defmodule MyApp.Blog.PostTag do
use Ecto.Schema
schema "post_tags" do
belongs_to :post, MyApp.Blog.Post
belongs_to :tag, MyApp.Blog.Tag
timestamps()
end
end
# Parent changeset — use cast_assoc with the join schema
def changeset(post, attrs) do
post
|> cast(attrs, [:title])
|> validate_required([:title])
|> cast_assoc(:post_tags, with: &PostTag.changeset/2)
endWhen updating a nested association with only some fields, preload the association first and rely on Ecto's internal ID matching — do not require :id in the child changeset:
def update_post(post, %{post: post_attrs, comments: comments_attrs}) do
post
|> Repo.preload(:comments)
|> Post.changeset(%{post_attrs | comments: comments_attrs})
|> Repo.update()
end