igmarin/elixir-phoenix-skills

elixir-essentials

MANDATORY for ALL Elixir code changes. Invoke before writing any .ex or .exs file. Enforces pragmatic Functional Core, Imperative Shell (FCIS): pure core modules, pattern matching, tagged tuples + with, linear pipes, explicit structs at boundaries, and thin edges. No monads or academic FP. Trigger words: elixir, FCIS, pattern matching, pipe, with, error handling, tagged tuples, guards, pure functions.

First seen Jun 20, 2026

Installation

$ npx skills add igmarin/elixir-phoenix-skills --skill elixir-essentials

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from igmarin/elixir-phoenix-skills · top by installs.

npx skills add igmarin/elixir-phoenix-skills

Browse all from igmarin/elixir-phoenix-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 2
License LICENSE
Default branch main
Open issues 0
Status Active

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.0.0
LicenseMIT
More metadata
version
1.0.0
user-invocable
true

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 8,864 B
  • docs SUMMARY.md 429 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 26 installs

SKILL.md

Elixir Essentials

Use this skill before writing any .ex or .exs file.

Canonical standard: [docs/fcis-engineering-rules.md](../../docs/fcis-engineering-rules.md).

Quick reference: [FCIS checklist](assets/fcis_checklist.md) — run before shipping .ex / .exs files.

Quick Reference

Concern Do this
Business rules MyApp.<Context>.<Concept> — no Repo / HTTP / process sends
Fallible flows `{:ok, _} \ {:error, _} + with`
Control flow Multi-clause + guards, not nested if
Transforms Linear pipes; named steps
External input Parse to struct/changeset at the edge
Callbacks @impl true; keep thin
Checklist [assets/fcischecklist.md](assets/fcischecklist.md)

RULES — Follow these with no exceptions

1. Functional Core, Imperative Shell — pure functions for rules/transforms; DB/HTTP/process/IO only at edges 2. Pattern match and guards over nested if / unless / deep case 3. Tagged tuples — fallible ops return {:ok, result} | {:error, reason}; chain with with 4. Linear pipes — subject first; named steps; no |> case do. then/2 is fine for a one-off; extract a named function when the step is reused or is a domain concept 5. Parse at the boundary — coerce maps into structs/changesets before pure core 6. Context is the shell — MyApp.Blog fetches and persists; MyApp.Blog.Publishing / MyApp.Orders.Pricing hold rules 7. @impl true on every behaviour callback 8. Immutability — never mutate data in place 9. Predicates end with ?, bang (!) only for dangerous/raising ops 10. No String.to_atom/1 on user input — prefer strings; allowlist before any atom conversion 11. No monads / category-theory libs — idiomatic Elixir only (with, tagged tuples, structs) 12. @doc / @moduledoc on public APIs 13. Prefer for over chaining 3+ Enum passes when one pass is clearer

1. Functional Core, Imperative Shell

❌ Bad: business rules mixed with side effects

def checkout(order_id) do
  order = Repo.get!(Order, order_id)
  total = Enum.reduce(order.lines, 0, &(&1.amount + &2))
  Payments.charge(order, total)
end

✅ Good: pure core + thin shell

defmodule MyApp.Orders.Pricing do
  def total(%{lines: lines}), do: Enum.reduce(lines, 0, &(&1.amount + &2))

  def discount(total, :vip) when total > 100, do: div(total, 10)
  def discount(_total, _tier), do: 0
end

defmodule MyApp.Orders do
  alias MyApp.Orders.Pricing

  def checkout(order_id) do
    with {:ok, order} <- fetch(order_id),
         total <- Pricing.total(order),
         {:ok, charge} <- Payments.charge(order, total) do
      {:ok, charge}
    end
  end
end

2. Pattern matching & guards

❌ Bad: nested conditionals

def handle_response(%{status: s} = r) do
  if s == 200, do: {:ok, r.body}, else: {:error, :bad_status}
end

✅ Good: multi-clause dispatch (pure mapper — not a behaviour callback)

def handle_response(%{status: 200, body: body}), do: {:ok, body}
def handle_response(%{status: 404}), do: {:error, :not_found}
def handle_response(%{status: status}), do: {:error, {:bad_status, status}}

def calculate(x) when is_integer(x) and x > 0, do: x * 2
def calculate(_), do: {:error, :invalid_input}

3. Railway flow: tagged tuples + with

❌ Bad: nested case

def create_post(params) do
  case validate(params) do
    {:ok, attrs} ->
      case Repo.insert(Post.changeset(%Post{}, attrs)) do
        {:ok, post} -> {:ok, post}
        error -> error
      end
    error -> error
  end
end

✅ Good: with for sequential fallible steps

def create_post(params) do
  with {:ok, attrs} <- validate(params)  # e.g. Blog.validate_post_attrs/1 (not shown),
       {:ok, post} <- Repo.insert(Post.changeset(%Post{}, attrs)) do
    {:ok, post}
  end
end

✅ Good: optional else for normalized edge errors

def transfer(from_id, to_id, amount) do
  with {:ok, from} <- get_account(from_id),
       {:ok, to} <- get_account(to_id),
       :ok <- ensure_funds(from, amount),
       {:ok, _} <- debit(from, amount),
       {:ok, _} <- credit(to, amount) do
    {:ok, :complete}
  else
    {:error, :insufficient_funds} -> {:error, :insufficient_funds}
    {:error, :not_found} -> {:error, :not_found}
    other -> {:error, {:transfer_failed, other}}
  end
end

4. Pipe linearity

❌ Bad: pipe into case

params
|> case do
  %{"id" => id} -> Repo.get(User, id)
  _ -> nil
end

✅ Good: linear named steps

params
|> Map.fetch!("id")
|> Users.get()

Pipes should read as one subject transformed. then/2 (and tap/2 for debug/side-log) is idiomatic for a one-off. Extract a named function when the step is reused or names a domain concept. Never |> case do.

5. Explicit shapes at the boundary

❌ Bad: untyped maps deep in core logic

def register(params) do
  email = params["email"]
  create_user(%{email: email, role: String.to_atom(params["role"])})
end

✅ Good: parse once, then use a known shape

def register(params) when is_map(params) do
  case Registration.changeset(params) |> Ecto.Changeset.apply_action(:insert) do
    {:ok, data} -> create_user(data)
    {:error, cs} -> {:error, cs}
  end
end

Naming

Element Convention Example
Modules PascalCase MyApp.Accounts.User
Functions / vars snake_case create_user/1
Predicates end with ? valid?/1
Raising end with ! get_user!/1 — avoid in app logic

Context vs core

The context module is the imperative shell. Business rules live in a sibling core module — not in the LiveView, worker, or the context's Repo function.

lib/my_app/blog.ex              # shell: fetch, persist, enqueue
lib/my_app/blog/post.ex         # schema
lib/my_app/blog/publishing.ex   # pure core: can_publish?/1, apply/1
lib/my_app/orders.ex            # shell
lib/my_app/orders/pricing.ex    # pure core: total/1, with_discount/2
# ✅ Shell
def publish_post(%Scope{} = scope, post) do
  with :ok <- Publishing.ensure_publishable(post),
       {:ok, post} <- persist_published(scope, post) do
    {:ok, post}
  end
end

# ✅ Core — no Repo
defmodule MyApp.Blog.Publishing do
  def ensure_publishable(%{status: :draft, title: title}) when byte_size(title) > 0, do: :ok
  def ensure_publishable(_post), do: {:error, :not_publishable}
end

List work

❌ Bad: three passes over the same list

list
|> Enum.map(&transform/1)
|> Enum.filter(&valid?/1)
|> Enum.map(&format/1)

✅ Good: one pass when clearer

for item <- list,
    transformed = transform(item),
    valid?(transformed) do
  format(transformed)
end

Atoms & user input

❌ Don't ✅ Do
String.toatom(userinput) Keep strings, or allowlist then convert
Blind toexistingatom/1 on arbitrary input Allowlist first — unknown values raise ArgumentError

Let it crash

Do not write defensive code for impossible states after you have validated at the boundary. Supervisors recover process failures; pure code should not hide bugs with broad rescue.

Common pitfalls

❌ Don't ✅ Do
Business math mixed with Repo MyApp.Orders.Pricing + thin MyApp.Orders shell
Nested if / case for sequential fallible steps with + tagged tuples
` > case do` Named step or multi-clause function
Mutate maps/lists in place Return new values
Skip @impl true on callbacks Annotate every callback
Monads / custom FP frameworks Idiomatic Elixir

Integration

Predecessor This Skill Successor
None (always first) elixir-essentials otp-essentials
None (always first) elixir-essentials testing-essentials
None (always first) elixir-essentials typespec-dialyzer

See also: docs/fcis-engineering-rules.md, otp-essentials, domain skills (Ecto/LiveView/Oban keep edges thin).