Skip to content
Back to skills

Elixir Coding Standards

ASecurity

Production-ready Elixir coding standards with OTP and Phoenix best practices. Use when enforcing elixir coding conventions and style rules.

  • 9 stars
  • 0 votes
  • 0 copies
  • 6 views
  • Added September 8, 2026
developmentgotestinggitapiperformancedocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 8, 2026

npx -y skills add anubhavg-icpl/vibe --skill elixir-coding-standards --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Elixir Coding Standards?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Elixir Coding Standards
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/anubhavg-icpl-elixir-coding-standards/badge)](https://www.skillsdirectory.com/skills/anubhavg-icpl-elixir-coding-standards)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: elixir-coding-standards
description: Production-ready Elixir coding standards with OTP and Phoenix best practices. Use when enforcing elixir coding conventions and style rules.
license: CC-BY-NC-SA-4.0
metadata:
  risk: unknown
  source: community
  kind: mode
  category: coding-standards
  tags: [elixir, otp, phoenix, standards, functional]
---

# Elixir Coding Standards

Production-ready coding standards for Elixir applications following OTP principles and community conventions.

## Naming Conventions

```elixir
# Modules: PascalCase
defmodule MyApp.Users.UserService do
end

# Functions/variables: snake_case
def get_user_by_id(user_id) do
end

# Private functions: prefixed with underscore internally referenced
defp do_process_user(user) do
end

# Atoms: snake_case
:user_not_found
:ok
:error

# Module attributes: @snake_case
@default_timeout 5000
@max_retries 3
```

## Code Organization

```elixir
defmodule MyApp.Users.User do
  @moduledoc """
  User schema and changeset functions.

  This module defines the User struct and provides functions
  for creating and validating user data.
  """

  use Ecto.Schema
  import Ecto.Changeset

  alias MyApp.Accounts.Team

  # 1. Module attributes
  @primary_key {:id, :binary_id, autogenerate: true}
  @derive {Jason.Encoder, only: [:id, :email, :name]}

  # 2. Schema definition
  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string, virtual: true, redact: true
    field :password_hash, :string, redact: true

    belongs_to :team, Team
    has_many :posts, MyApp.Content.Post

    timestamps()
  end

  # 3. Type specs
  @type t :: %__MODULE__{
    id: Ecto.UUID.t(),
    email: String.t(),
    name: String.t()
  }

  # 4. Public functions
  @doc """
  Creates a changeset for user registration.
  """
  @spec registration_changeset(t(), map()) :: Ecto.Changeset.t()
  def registration_changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :name, :password])
    |> validate_required([:email, :name, :password])
    |> validate_email()
    |> validate_password()
    |> hash_password()
  end

  # 5. Private functions
  defp validate_email(changeset) do
    changeset
    |> validate_format(:email, ~r/^[^\s]+@[^\s]+$/)
    |> validate_length(:email, max: 160)
    |> unsafe_validate_unique(:email, MyApp.Repo)
    |> unique_constraint(:email)
  end

  defp validate_password(changeset) do
    changeset
    |> validate_length(:password, min: 12, max: 72)
    |> validate_format(:password, ~r/[a-z]/, message: "must have lowercase")
    |> validate_format(:password, ~r/[A-Z]/, message: "must have uppercase")
    |> validate_format(:password, ~r/[0-9]/, message: "must have digit")
  end

  defp hash_password(changeset) do
    case get_change(changeset, :password) do
      nil -> changeset
      password -> put_change(changeset, :password_hash, Bcrypt.hash_pwd_salt(password))
    end
  end
end
```

## Pattern Matching & Guards

```elixir
# Use pattern matching over conditionals
# Good
def handle_result({:ok, value}), do: process(value)
def handle_result({:error, reason}), do: log_error(reason)

# Avoid
def handle_result(result) do
  if elem(result, 0) == :ok do
    process(elem(result, 1))
  else
    log_error(elem(result, 1))
  end
end

# Use guards for type checks
def process_data(data) when is_map(data), do: Map.keys(data)
def process_data(data) when is_list(data), do: length(data)
def process_data(_), do: {:error, :invalid_type}

# Multi-clause functions for different arities
def send_notification(user), do: send_notification(user, [])
def send_notification(user, opts) when is_list(opts) do
  # implementation
end
```

## Error Handling

```elixir
# Use tagged tuples
@spec get_user(String.t()) :: {:ok, User.t()} | {:error, :not_found | :invalid_id}
def get_user(id) do
  case Repo.get(User, id) do
    nil -> {:error, :not_found}
    user -> {:ok, user}
  end
end

# Use with for happy path
def create_order(user_id, items) do
  with {:ok, user} <- get_user(user_id),
       {:ok, validated_items} <- validate_items(items),
       {:ok, order} <- build_order(user, validated_items),
       {:ok, saved_order} <- Repo.insert(order) do
    {:ok, saved_order}
  else
    {:error, :not_found} -> {:error, "User not found"}
    {:error, :invalid_items} -> {:error, "Invalid items"}
    {:error, changeset} -> {:error, format_errors(changeset)}
  end
end

# Avoid bare rescue
# Good
try do
  risky_operation()
rescue
  e in RuntimeError -> {:error, e.message}
  e in ArgumentError -> {:error, "Invalid argument: #{e.message}"}
end

# Let it crash for unexpected errors
def process(data) do
  # If this fails, let the supervisor handle it
  do_process!(data)
end
```

## OTP Patterns

```elixir
# GenServer with proper callbacks
defmodule MyApp.Cache do
  use GenServer
  require Logger

  # Client API - Public interface
  def start_link(opts \\ []) do
    name = Keyword.get(opts, :name, __MODULE__)
    GenServer.start_link(__MODULE__, opts, name: name)
  end

  def get(key, server \\ __MODULE__) do
    GenServer.call(server, {:get, key})
  end

  def put(key, value, server \\ __MODULE__) do
    GenServer.cast(server, {:put, key, value})
  end

  # Server callbacks
  @impl true
  def init(opts) do
    ttl = Keyword.get(opts, :ttl, :timer.minutes(5))
    schedule_cleanup(ttl)
    {:ok, %{cache: %{}, ttl: ttl}}
  end

  @impl true
  def handle_call({:get, key}, _from, %{cache: cache} = state) do
    {:reply, Map.get(cache, key), state}
  end

  @impl true
  def handle_cast({:put, key, value}, %{cache: cache} = state) do
    {:noreply, %{state | cache: Map.put(cache, key, {value, System.monotonic_time()})}}
  end

  @impl true
  def handle_info(:cleanup, %{cache: cache, ttl: ttl} = state) do
    now = System.monotonic_time()
    new_cache = Enum.filter(cache, fn {_k, {_v, time}} ->
      System.convert_time_unit(now - time, :native, :millisecond) < ttl
    end) |> Map.new()

    schedule_cleanup(ttl)
    {:noreply, %{state | cache: new_cache}}
  end

  defp schedule_cleanup(ttl), do: Process.send_after(self(), :cleanup, ttl)
end
```

## Testing Standards

```elixir
defmodule MyApp.Users.UserTest do
  use MyApp.DataCase, async: true

  alias MyApp.Users
  alias MyApp.Users.User

  describe "create_user/1" do
    test "creates user with valid attributes" do
      attrs = %{email: "test@example.com", name: "Test", password: "SecurePass123"}

      assert {:ok, %User{} = user} = Users.create_user(attrs)
      assert user.email == "test@example.com"
      assert user.password_hash != nil
    end

    test "returns error with invalid email" do
      attrs = %{email: "invalid", name: "Test", password: "SecurePass123"}

      assert {:error, changeset} = Users.create_user(attrs)
      assert "has invalid format" in errors_on(changeset).email
    end
  end

  describe "get_user/1" do
    setup do
      user = insert(:user)
      %{user: user}
    end

    test "returns user when exists", %{user: user} do
      assert {:ok, found} = Users.get_user(user.id)
      assert found.id == user.id
    end

    test "returns error when not found" do
      assert {:error, :not_found} = Users.get_user(Ecto.UUID.generate())
    end
  end
end
```

## Documentation

```elixir
@moduledoc """
Handles user authentication and session management.

## Examples

    iex> MyApp.Auth.authenticate("user@example.com", "password")
    {:ok, %User{}}

    iex> MyApp.Auth.authenticate("user@example.com", "wrong")
    {:error, :invalid_credentials}

## Configuration

    config :my_app, MyApp.Auth,
      token_ttl: :timer.hours(24),
      max_sessions: 5
"""

@doc """
Authenticates a user by email and password.

Returns `{:ok, user}` on success or `{:error, reason}` on failure.

## Parameters

  * `email` - The user's email address
  * `password` - The plaintext password to verify

## Examples

    iex> authenticate("valid@email.com", "correct_password")
    {:ok, %User{email: "valid@email.com"}}

    iex> authenticate("valid@email.com", "wrong_password")
    {:error, :invalid_credentials}

"""
@spec authenticate(String.t(), String.t()) :: {:ok, User.t()} | {:error, atom()}
def authenticate(email, password) do
  # implementation
end
```

## Phoenix Specific

```elixir
# Controllers - thin, delegate to contexts
defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller

  alias MyApp.Users

  action_fallback MyAppWeb.FallbackController

  def index(conn, params) do
    users = Users.list_users(params)
    render(conn, :index, users: users)
  end

  def create(conn, %{"user" => user_params}) do
    with {:ok, user} <- Users.create_user(user_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", ~p"/api/users/#{user}")
      |> render(:show, user: user)
    end
  end
end

# LiveView - keep state minimal
defmodule MyAppWeb.UserLive.Index do
  use MyAppWeb, :live_view

  @impl true
  def mount(_params, _session, socket) do
    {:ok, stream(socket, :users, Users.list_users())}
  end

  @impl true
  def handle_event("delete", %{"id" => id}, socket) do
    user = Users.get_user!(id)
    {:ok, _} = Users.delete_user(user)
    {:noreply, stream_delete(socket, :users, user)}
  end
end
```

## Performance Guidelines

1. **Use streams for large collections**
2. **Avoid N+1 queries with preloads**
3. **Use async operations for I/O**
4. **Cache expensive computations**
5. **Use ETS for read-heavy caches**

## Credo Configuration

```elixir
# .credo.exs
%{
  configs: [
    %{
      name: "default",
      checks: [
        {Credo.Check.Readability.MaxLineLength, max_length: 120},
        {Credo.Check.Design.TagTODO, exit_status: 0},
        {Credo.Check.Refactor.Nesting, max_nesting: 3}
      ]
    }
  ]
}
```

Follow these standards for maintainable, idiomatic Elixir code.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.