🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Elixir Dev

by @gchapim

Elixir/Phoenix development companion. Run and interpret mix test, mix credo, mix dialyzer, mix format. Generate modules following OTP conventions: contexts, schemas, GenServers, supervisors, tasks. Debug compilation errors and warnings. Help with Ecto migrations, queries, changesets, and associations. Use for any Elixir or Phoenix development task including writing modules, fixing tests, refactoring code, or understanding OTP patterns.

Versionv1.0.0
Downloads2,130
Installs2
Stars⭐ 1
TERMINAL
clawhub install elixir-dev

πŸ“– About This Skill


name: elixir-dev description: "Elixir/Phoenix development companion. Run and interpret mix test, mix credo, mix dialyzer, mix format. Generate modules following OTP conventions: contexts, schemas, GenServers, supervisors, tasks. Debug compilation errors and warnings. Help with Ecto migrations, queries, changesets, and associations. Use for any Elixir or Phoenix development task including writing modules, fixing tests, refactoring code, or understanding OTP patterns."

Elixir Dev

Running Mix Commands

See references/mix-commands.md for full command reference.

Test

# Run all tests
mix test

Specific file or line

mix test test/my_app/accounts_test.exs:42

By tag

mix test --only integration

Failed only (requires --failed flag from prior run)

mix test --failed

With coverage

mix test --cover

Interpreting failures:

  • ** (MatchError) β€” Pattern match failed; check return value shape.
  • ** (Ecto.NoResultsError) β€” Repo.get! with non-existent ID; use Repo.get or seed data.
  • ** (DBConnection.OwnershipError) β€” Missing async: true or sandbox setup.
  • no function clause matching β€” Wrong arity or unexpected arg type.
  • Credo

    mix credo --strict
    mix credo suggest --format json
    mix credo explain MyApp.Module  # Explain issues for specific module
    

    Common Credo fixes:

  • Credo.Check.Readability.ModuleDoc β€” Add @moduledoc.
  • Credo.Check.Refactor.CyclomaticComplexity β€” Extract helper functions.
  • Credo.Check.Design.TagTODO β€” Address or remove TODO comments.
  • Dialyzer

    mix dialyzer
    mix dialyzer --format short
    

    Common Dialyzer warnings:

  • The pattern can never match β€” Dead code or wrong type in pattern.
  • Function has no local return β€” Crashes on all paths; check internal calls.
  • The call will never return β€” Calling a function that always raises.
  • Fix: Add @spec annotations; use @dialyzer {:nowarn_function, func: arity} as last resort.
  • Format

    mix format
    mix format --check-formatted  # CI mode β€” exit 1 if unformatted
    

    Module Generation

    Always include @moduledoc, @doc, and @spec on public functions.

    Context Module

    defmodule MyApp.Notifications do
      @moduledoc """
      Manages notification delivery and preferences.
      """
      import Ecto.Query
      alias MyApp.Repo
      alias MyApp.Notifications.Notification

    @doc "List notifications for a user, most recent first." @spec list_notifications(String.t(), keyword()) :: [Notification.t()] def list_notifications(user_id, opts \\ []) do limit = Keyword.get(opts, :limit, 50)

    Notification |> where(user_id: ^user_id) |> order_by(desc: :inserted_at) |> limit(^limit) |> Repo.all() end end

    Schema Module

    defmodule MyApp.Notifications.Notification do
      @moduledoc """
      Schema for push/email/sms notifications.
      """
      use Ecto.Schema
      import Ecto.Changeset

    @type t :: %__MODULE__{}

    @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id @timestamps_opts [type: :utc_datetime_usec]

    schema "notifications" do field :channel, Ecto.Enum, values: [:push, :email, :sms] field :title, :string field :body, :string field :delivered_at, :utc_datetime_usec field :user_id, :binary_id

    timestamps() end

    @required ~w(channel title body user_id)a

    @doc false def changeset(notification, attrs) do notification |> cast(attrs, @required ++ [:delivered_at]) |> validate_required(@required) |> validate_length(:title, max: 255) end end

    OTP Patterns

    See references/otp-patterns.md for GenServer, Supervisor, Agent, Task patterns.

    When to Use What

    | Pattern | Use When | |---------|----------| | GenServer | Stateful process with sync/async calls (cache, rate limiter, connection pool) | | Agent | Simple state wrapper with no complex logic | | Task | One-off async work, fire-and-forget or awaited | | Task.Supervisor | Supervised fire-and-forget tasks | | Supervisor | Managing child process lifecycles | | Registry | Process lookup by name/key | | DynamicSupervisor | Starting children at runtime |

    GenServer Template

    defmodule MyApp.RateLimiter do
      @moduledoc "Token bucket rate limiter."
      use GenServer

    # Client API def start_link(opts) do name = Keyword.get(opts, :name, __MODULE__) GenServer.start_link(__MODULE__, opts, name: name) end

    @spec check_rate(String.t()) :: :ok | {:error, :rate_limited} def check_rate(key), do: GenServer.call(__MODULE__, {:check, key})

    # Server callbacks @impl true def init(opts) do {:ok, %{limit: Keyword.get(opts, :limit, 100), window_ms: 60_000, buckets: %{}}} end

    @impl true def handle_call({:check, key}, _from, state) do now = System.monotonic_time(:millisecond) {count, state} = increment(state, key, now) if count <= state.limit, do: {:reply, :ok, state}, else: {:reply, {:error, :rate_limited}, state} end

    defp increment(state, key, now) do # Implementation end end

    Common Compilation Errors

    | Error | Cause | Fix | |-------|-------|-----| | module X is not available | Missing dep or typo | Check mix.exs deps, verify module name | | undefined function X/N | Not imported/aliased | Add import, alias, or full module path | | (CompileError) redefining module | Duplicate module name | Rename one of them | | protocol not implemented | Missing protocol impl | Add defimpl for your struct | | cannot use ^x outside of match | Pin in wrong position | Move to pattern match context |

    Ecto Query Patterns

    Dynamic Filters

    def list(filters) do
      Enum.reduce(filters, base_query(), fn
        {:status, val}, q -> where(q, [r], r.status == ^val)
        {:since, dt}, q -> where(q, [r], r.inserted_at >= ^dt)
        {:search, term}, q -> where(q, [r], ilike(r.name, ^"%#{term}%"))
        _, q -> q
      end)
      |> Repo.all()
    end
    

    Preloading

    # Query-time preload (single query with join)
    from(p in Post, join: a in assoc(p, :author), preload: [author: a])

    Separate query preload

    Post |> Repo.all() |> Repo.preload(:author)

    Nested

    Repo.preload(posts, [comments: :author])

    Aggregates

    from(o in Order,
      where: o.tenant_id == ^tenant_id,
      group_by: o.status,
      select: {o.status, count(o.id), sum(o.amount)}
    )
    |> Repo.all()
    

    Phoenix LiveView Basics

    Mount + Handle Events

    defmodule MyAppWeb.DashboardLive do
      use MyAppWeb, :live_view

    @impl true def mount(_params, _session, socket) do {:ok, assign(socket, items: [], loading: true)} end

    @impl true def handle_event("delete", %{"id" => id}, socket) do MyApp.Items.delete_item!(id) {:noreply, assign(socket, items: MyApp.Items.list_items())} end

    @impl true def render(assigns) do ~H"""

    <%= item.name %>
    """ end end

    PubSub for Real-time

    # Subscribe in mount
    def mount(_, _, socket) do
      if connected?(socket), do: Phoenix.PubSub.subscribe(MyApp.PubSub, "items")
      {:ok, assign(socket, items: list_items())}
    end

    Broadcast from context

    def create_item(attrs) do with {:ok, item} <- %Item{} |> Item.changeset(attrs) |> Repo.insert() do Phoenix.PubSub.broadcast(MyApp.PubSub, "items", {:item_created, item}) {:ok, item} end end

    Handle in LiveView

    def handle_info({:item_created, item}, socket) do {:noreply, update(socket, :items, &[item | &1])} end