Elixir Standards
Elixir Standards
Section titled “Elixir Standards”1. Project Management
Section titled “1. Project Management”- Tool: Mix (
mix).mix.exsdefines the project and its dependencies. - Version: Elixir 1.15+ / OTP 26+ (use latest stable).
- Lock Files:
mix.lockis always committed.
2. Package Management
Section titled “2. Package Management”- Registry: Hex (
hex.pm). Add packages todeps/0inmix.exs. - Security: Run
mix hex.auditin CI to check for vulnerable packages. - Private packages: Use Hex organizations or Git dependencies for private code.
3. Code Style
Section titled “3. Code Style”- Formatter:
mix formatis mandatory. Commit only formatted code. Addmix format --check-formattedto CI. - Linter: Credo (
mix credo --strict). Fix all issues before merging. - Type analysis: Dialyxir (
mix dialyzer). Run in CI. Treat all warnings as errors.
.credo.exs (minimum)
Section titled “.credo.exs (minimum)”%{ configs: [ %{ name: "default", strict: true, checks: %{ enabled: [ {Credo.Check.Design.AliasUsage, []}, {Credo.Check.Readability.ModuleDoc, []}, {Credo.Check.Warning.IExPry, []} ] } } ]}4. Naming Conventions
Section titled “4. Naming Conventions”- Modules:
PascalCase(e.g.,MyApp.UserService) - Functions and variables:
snake_case - Atoms:
:snake_case - Files:
snake_case.ex/snake_case_test.exs - Predicates: End with
?(e.g.,valid?/1) - Bang functions: End with
!for versions that raise on error (e.g.,get!/1) - Constants: Use module attributes (
@max_retries 3), not uppercase variables
5. Project Structure
Section titled “5. Project Structure”lib/├── my_app/│ ├── application.ex # OTP Application entry point│ ├── repo.ex # Ecto Repo (if using database)│ ├── domain/ # Business logic│ │ ├── user.ex│ │ └── user_policy.ex│ └── service/│ └── user_service.extest/├── my_app/│ └── domain/│ └── user_test.exs└── test_helper.exsconfig/├── config.exs # Shared configuration├── dev.exs├── test.exs└── runtime.exs # Runtime/prod configuration (secrets via env vars)6. Pattern Matching
Section titled “6. Pattern Matching”Pattern matching is idiomatic Elixir. Prefer it over conditional chains.
- Match in function heads to dispatch on input shape instead of using
if/condinside a single function. casefor local branching on a value.withfor chaining multiple pattern-matching steps where any can fail.
# Prefer: function head matchingdef process(%{status: :active} = user), do: activate(user)def process(%{status: :inactive} = user), do: deactivate(user)
# Prefer: with for sequential operationsdef create_user(attrs) do with {:ok, changeset} <- validate(attrs), {:ok, user} <- Repo.insert(changeset), :ok <- send_welcome_email(user) do {:ok, user} endend7. Pipe Operator
Section titled “7. Pipe Operator”- Use
|>to express sequential data transformations. - Each pipe step should do one clear thing.
- Do not start a pipeline with a bare variable on its own line — begin with the data source.
def process_order(order) do order |> validate_items() |> calculate_total() |> apply_discounts() |> charge_customer()end8. OTP Patterns
Section titled “8. OTP Patterns”- GenServer: Use for stateful processes. Implement
handle_call/3,handle_cast/2, andhandle_info/2. - Supervisor: Wrap all long-lived processes in a Supervisor. Choose a restart strategy (
one_for_one,one_for_all,rest_for_one). - Application: Define the top-level supervision tree in
MyApp.Application. - Task: Use
Taskfor one-off async work. UseTask.Supervisorfor dynamic tasks that need fault isolation. - Registry: Use
Registryfor named process lookups instead of global atoms.
defmodule MyApp.UserCache do use GenServer
def start_link(opts), do: GenServer.start_link(__MODULE__, %{}, opts)
def get(pid, key), do: GenServer.call(pid, {:get, key})
@impl GenServer def init(state), do: {:ok, state}
@impl GenServer def handle_call({:get, key}, _from, state) do {:reply, Map.get(state, key), state} endend9. Testing
Section titled “9. Testing”- Framework: ExUnit. Run with
mix test. - Describe blocks: Group related tests with
describe/2. - Async tests: Mark modules with
async: truewhen tests have no shared state. - Factories: Use
ex_machinafor test data factories instead of raw fixture files. - Mocking: Use
moxfor behaviour-based mocks. Define behaviours for external dependencies. - Coverage:
mix test --cover. 95% minimum for any module. Target 100% for domain logic.
defmodule MyApp.UserTest do use ExUnit.Case, async: true
describe "validate/1" do test "returns :ok for valid attrs" do assert {:ok, _user} = MyApp.User.validate(%{email: "test@example.com", name: "Alice"}) end
test "returns error for missing email" do assert {:error, changeset} = MyApp.User.validate(%{name: "Alice"}) assert "can't be blank" in errors_on(changeset).email end endend10. Documentation
Section titled “10. Documentation”@moduledoc: Required on every module. Use@moduledoc falseonly for internal modules that are implementation details.@doc: Required on every public function. Describe what it does, its arguments, and its return values.@spec: Required on every public function. Dialyzer uses specs for analysis.- Examples: Include
## Exampleswithiex>doctests in@docfor pure functions.
defmodule MyApp.User do @moduledoc """ Domain type and business rules for user management. """
@doc """ Validates user attributes and returns a changeset.
## Examples
iex> MyApp.User.validate(%{email: "test@example.com", name: "Alice"}) {:ok, %MyApp.User{}}
""" @spec validate(map()) :: {:ok, t()} | {:error, Ecto.Changeset.t()} def validate(attrs), do: # ...end11. Phoenix Conventions
Section titled “11. Phoenix Conventions”When using Phoenix:
- Contexts: Group domain logic into Phoenix Contexts (
MyApp.Accounts,MyApp.Orders). Controllers call context functions, never Ecto directly. - Controllers: Thin — delegate to context functions. No business logic in controllers.
- LiveView: Use
assign/3for state. Extract reusable UI into function components (Phoenix.Component). - PubSub: Use
Phoenix.PubSubfor broadcasting events across processes. - Channels: Use Phoenix Channels for bidirectional real-time communication.
- Plugs: Use
Plugfor shared request pipeline logic (authentication, logging, etc.).
12. Security
Section titled “12. Security”- Secrets: Never hardcode secrets. Use
config/runtime.exswithSystem.fetch_env!/1. - SQL injection: Use Ecto parameterized queries exclusively. Never interpolate input into query strings.
- XSS: Phoenix templates auto-escape HTML by default. Use
raw/1only when output is known-safe. - CSRF: Phoenix includes CSRF protection by default. Do not disable it.
- Input validation: Use Ecto changesets to validate and cast all external input.
- SAST: Run
mix credo --strictandmix sobelow(Phoenix security-focused SAST) in CI. - Dependency scanning: Run
mix hex.auditin CI. - See sec-01_security_standards.md for the complete banned-functions list with language-specific examples.