Claude Code Plugins

Community-maintained marketplace

Feedback

moai-lang-elixir

@junseokandylee/RallyApp
1
0

Elixir 1.17+ development specialist covering Phoenix 1.7, LiveView, Ecto, and OTP patterns. Use when developing real-time applications, distributed systems, or Phoenix projects.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name moai-lang-elixir
description Elixir 1.17+ development specialist covering Phoenix 1.7, LiveView, Ecto, and OTP patterns. Use when developing real-time applications, distributed systems, or Phoenix projects.
version 1.0.0
updated Sun Dec 07 2025 00:00:00 GMT+0000 (Coordinated Universal Time)
status active
allowed-tools Read, Grep, Glob, Bash, mcp__context7__resolve-library-id, mcp__context7__get-library-docs

Quick Reference (30 seconds)

Elixir 1.17+ Development Specialist - Phoenix 1.7, LiveView, Ecto, OTP patterns, and functional programming.

Auto-Triggers: .ex, .exs files, mix.exs, config/, Phoenix/LiveView discussions

Core Capabilities:

  • Elixir 1.17: Pattern matching, pipes, protocols, behaviours, macros
  • Phoenix 1.7: Controllers, LiveView, Channels, PubSub, Verified Routes
  • Ecto: Schemas, Changesets, Queries, Migrations, Multi
  • OTP: GenServer, Supervisor, Agent, Task, Registry
  • ExUnit: Testing with setup, describe, async
  • Mix: Build tool, tasks, releases
  • Oban: Background job processing

Quick Patterns

Phoenix Controller:

defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller

  alias MyApp.Accounts

  def show(conn, %{"id" => id}) do
    user = Accounts.get_user!(id)
    render(conn, :show, user: user)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created successfully.")
        |> redirect(to: ~p"/users/#{user}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end
end

Ecto Schema with Changeset:

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :email, :string
    field :password_hash, :string
    field :password, :string, virtual: true

    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> validate_format(:email, ~r/@/)
    |> validate_length(:password, min: 8)
    |> unique_constraint(:email)
    |> put_password_hash()
  end
end

GenServer Pattern:

defmodule MyApp.Counter do
  use GenServer

  def start_link(initial_value) do
    GenServer.start_link(__MODULE__, initial_value, name: __MODULE__)
  end

  def increment, do: GenServer.call(__MODULE__, :increment)
  def get_count, do: GenServer.call(__MODULE__, :get)

  @impl true
  def init(initial_value), do: {:ok, initial_value}

  @impl true
  def handle_call(:increment, _from, count), do: {:reply, count + 1, count + 1}
  def handle_call(:get, _from, count), do: {:reply, count, count}
end

Implementation Guide (5 minutes)

Elixir 1.17 Features

Set-Theoretic Types (Gradual Typing):

  • Type annotations for better code documentation
  • Compile-time warnings for type mismatches
  • Enhanced dialyzer integration

Pattern Matching Advanced:

def process_message(%{type: :email, to: to} = message) when is_binary(to) do
  send_email(message)
end

def process_message(%{type: :sms, phone: phone}) when byte_size(phone) == 10 do
  send_sms(phone)
end

def process_message(_), do: {:error, :invalid_message}

Pipe Operator Best Practices:

def process_order(order_params) do
  order_params
  |> validate_order()
  |> calculate_total()
  |> apply_discounts()
  |> create_order()
end

# With error handling using with
def process_order_safe(params) do
  with {:ok, validated} <- validate_order(params),
       {:ok, total} <- calculate_total(validated),
       {:ok, discounted} <- apply_discounts(total),
       {:ok, order} <- create_order(discounted) do
    {:ok, order}
  else
    {:error, reason} -> {:error, reason}
  end
end

Protocols for Polymorphism:

defprotocol Stringify do
  @doc "Converts a data structure to string"
  def to_string(data)
end

defimpl Stringify, for: Map do
  def to_string(map), do: Jason.encode!(map)
end

defimpl Stringify, for: List do
  def to_string(list), do: Enum.join(list, ", ")
end

Phoenix 1.7 Patterns

LiveView Component:

defmodule MyAppWeb.CounterLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  def handle_event("increment", _, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}
  end

  def render(assigns) do
    ~H"""
    <div class="counter">
      <h1>Count: <%= @count %></h1>
      <button phx-click="increment">Increment</button>
    </div>
    """
  end
end

LiveView Form with Changesets:

defmodule MyAppWeb.UserFormLive do
  use MyAppWeb, :live_view

  alias MyApp.Accounts
  alias MyApp.Accounts.User

  def mount(_params, _session, socket) do
    changeset = Accounts.change_user(%User{})
    {:ok, assign(socket, form: to_form(changeset))}
  end

  def handle_event("validate", %{"user" => user_params}, socket) do
    changeset =
      %User{}
      |> Accounts.change_user(user_params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(changeset))}
  end

  def handle_event("save", %{"user" => user_params}, socket) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        {:noreply,
         socket
         |> put_flash(:info, "User created!")
         |> push_navigate(to: ~p"/users/#{user}")}

      {:error, %Ecto.Changeset{} = changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  def render(assigns) do
    ~H"""
    <.form for={@form} phx-change="validate" phx-submit="save">
      <.input field={@form[:name]} label="Name" />
      <.input field={@form[:email]} type="email" label="Email" />
      <.button>Save</.button>
    </.form>
    """
  end
end

Phoenix Channels:

defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  @impl true
  def join("room:" <> room_id, _params, socket) do
    send(self(), :after_join)
    {:ok, assign(socket, :room_id, room_id)}
  end

  @impl true
  def handle_info(:after_join, socket) do
    push(socket, "presence_state", MyAppWeb.Presence.list(socket))
    {:ok, _} = MyAppWeb.Presence.track(socket, socket.assigns.user_id, %{
      online_at: System.system_time(:second)
    })
    {:noreply, socket}
  end

  @impl true
  def handle_in("new_message", %{"body" => body}, socket) do
    broadcast!(socket, "new_message", %{
      user_id: socket.assigns.user_id,
      body: body,
      inserted_at: DateTime.utc_now()
    })
    {:noreply, socket}
  end
end

Verified Routes:

# In router.ex
scope "/", MyAppWeb do
  pipe_through :browser

  live "/users", UserLive.Index, :index
  live "/users/:id", UserLive.Show, :show
end

# Usage with ~p sigil
~p"/users"           # "/users"
~p"/users/#{user}"   # "/users/123"
~p"/users?page=1"    # "/users?page=1"

Ecto Advanced Patterns

Multi for Transactions:

def transfer_funds(from_account, to_account, amount) do
  Ecto.Multi.new()
  |> Ecto.Multi.update(:withdraw, withdraw_changeset(from_account, amount))
  |> Ecto.Multi.update(:deposit, deposit_changeset(to_account, amount))
  |> Ecto.Multi.insert(:transaction, fn %{withdraw: from, deposit: to} ->
    Transaction.changeset(%Transaction{}, %{
      from_account_id: from.id,
      to_account_id: to.id,
      amount: amount
    })
  end)
  |> Repo.transaction()
end

Query Composition:

defmodule MyApp.Accounts.UserQuery do
  import Ecto.Query

  def base, do: from(u in User)

  def active(query \\ base()) do
    from u in query, where: u.active == true
  end

  def by_email(query \\ base(), email) do
    from u in query, where: u.email == ^email
  end

  def with_posts(query \\ base()) do
    from u in query, preload: [:posts]
  end

  def order_by_recent(query \\ base()) do
    from u in query, order_by: [desc: u.inserted_at]
  end
end

# Usage
User
|> UserQuery.active()
|> UserQuery.with_posts()
|> UserQuery.order_by_recent()
|> Repo.all()

Embedded Schemas:

defmodule MyApp.Order do
  use Ecto.Schema
  import Ecto.Changeset

  schema "orders" do
    field :status, :string
    embeds_one :shipping_address, Address, on_replace: :update
    embeds_many :items, Item, on_replace: :delete

    timestamps()
  end

  def changeset(order, attrs) do
    order
    |> cast(attrs, [:status])
    |> cast_embed(:shipping_address, required: true)
    |> cast_embed(:items, required: true)
  end
end

defmodule MyApp.Order.Address do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :street, :string
    field :city, :string
    field :zip, :string
  end

  def changeset(address, attrs) do
    address
    |> cast(attrs, [:street, :city, :zip])
    |> validate_required([:street, :city, :zip])
  end
end

OTP Patterns

Supervisor Tree:

defmodule MyApp.Application do
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      MyApp.Repo,
      MyAppWeb.Telemetry,
      {Phoenix.PubSub, name: MyApp.PubSub},
      MyAppWeb.Endpoint,
      {MyApp.Cache, []},
      {Task.Supervisor, name: MyApp.TaskSupervisor},
      MyApp.SchedulerSupervisor
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Dynamic Supervisor:

defmodule MyApp.WorkerSupervisor do
  use DynamicSupervisor

  def start_link(init_arg) do
    DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  @impl true
  def init(_init_arg) do
    DynamicSupervisor.init(strategy: :one_for_one)
  end

  def start_worker(args) do
    spec = {MyApp.Worker, args}
    DynamicSupervisor.start_child(__MODULE__, spec)
  end

  def stop_worker(pid) do
    DynamicSupervisor.terminate_child(__MODULE__, pid)
  end
end

Registry for Named Processes:

# Start registry in application supervision tree
{Registry, keys: :unique, name: MyApp.Registry}

# GenServer with dynamic name
defmodule MyApp.Session do
  use GenServer

  def start_link(user_id) do
    GenServer.start_link(__MODULE__, user_id, name: via_tuple(user_id))
  end

  defp via_tuple(user_id) do
    {:via, Registry, {MyApp.Registry, {:session, user_id}}}
  end

  def get_session(user_id) do
    GenServer.call(via_tuple(user_id), :get)
  end
end

ExUnit Testing

Async Tests with Setup:

defmodule MyApp.AccountsTest do
  use MyApp.DataCase, async: true

  alias MyApp.Accounts

  describe "users" do
    setup do
      user = insert(:user)
      {:ok, user: user}
    end

    test "get_user!/1 returns the user with given id", %{user: user} do
      assert Accounts.get_user!(user.id) == user
    end

    test "create_user/1 with valid data creates a user" do
      valid_attrs = %{name: "Test", email: "test@example.com", password: "password123"}

      assert {:ok, %User{} = user} = Accounts.create_user(valid_attrs)
      assert user.name == "Test"
      assert user.email == "test@example.com"
    end

    test "create_user/1 with invalid data returns error changeset" do
      assert {:error, %Ecto.Changeset{}} = Accounts.create_user(%{})
    end
  end
end

LiveView Testing:

defmodule MyAppWeb.CounterLiveTest do
  use MyAppWeb.ConnCase

  import Phoenix.LiveViewTest

  test "renders counter", %{conn: conn} do
    {:ok, view, html} = live(conn, ~p"/counter")

    assert html =~ "Count: 0"
  end

  test "increments counter on click", %{conn: conn} do
    {:ok, view, _html} = live(conn, ~p"/counter")

    assert view
           |> element("button", "Increment")
           |> render_click() =~ "Count: 1"
  end
end

Oban Background Jobs

Job Worker:

defmodule MyApp.Workers.EmailWorker do
  use Oban.Worker, queue: :mailers, max_attempts: 3

  @impl Oban.Worker
  def perform(%Oban.Job{args: %{"email" => email, "template" => template}}) do
    case MyApp.Mailer.send_email(email, template) do
      {:ok, _} -> :ok
      {:error, reason} -> {:error, reason}
    end
  end
end

# Enqueue job
%{email: "user@example.com", template: "welcome"}
|> MyApp.Workers.EmailWorker.new()
|> Oban.insert()

# Scheduled job
%{email: "user@example.com", template: "reminder"}
|> MyApp.Workers.EmailWorker.new(scheduled_at: DateTime.add(DateTime.utc_now(), 3600))
|> Oban.insert()

Advanced Implementation (10+ minutes)

For comprehensive coverage including:

  • Production deployment with releases
  • Distributed systems with libcluster
  • Advanced LiveView patterns (streams, components)
  • Telemetry and observability
  • Security best practices
  • CI/CD integration patterns

See:


Context7 Library Mappings

/elixir-lang/elixir - Elixir language documentation
/phoenixframework/phoenix - Phoenix web framework
/phoenixframework/phoenix_live_view - LiveView real-time UI
/elixir-ecto/ecto - Database wrapper and query language
/sorentwo/oban - Background job processing

Works Well With

  • moai-domain-backend - REST API and microservices architecture
  • moai-domain-database - SQL patterns and query optimization
  • moai-quality-testing - TDD and testing strategies
  • moai-essentials-debug - AI-powered debugging
  • moai-platform-deploy - Deployment and infrastructure

Troubleshooting

Common Issues:

Elixir Version Check:

elixir --version  # Should be 1.17+
mix --version     # Mix build tool version

Dependency Issues:

mix deps.get      # Fetch dependencies
mix deps.compile  # Compile dependencies
mix clean         # Clean build artifacts

Database Migrations:

mix ecto.create   # Create database
mix ecto.migrate  # Run migrations
mix ecto.rollback # Rollback last migration

Phoenix Server:

mix phx.server           # Start server
iex -S mix phx.server    # Start with IEx
MIX_ENV=prod mix release # Build release

LiveView Not Loading:

  • Check websocket connection in browser console
  • Verify endpoint configuration for websocket
  • Ensure Phoenix.LiveView is in mix.exs dependencies

Last Updated: 2025-12-07 Status: Active (v1.0.0)