Skip to content
Documentation and @moduledoc / @doc
step 1/7

Reading — step 1 of 7

Learn

~3 min readMix and Testing

Elixir treats documentation as a first-class concern. Every project gets generated HTML docs from @moduledoc and @doc attributes. The Elixir docs themselves are a great example.

@moduledoc and @doc

defmodule MyApp.Math do
    @moduledoc """
    Math utilities for MyApp.
    
    Provides addition, subtraction, and division with rounded outputs.
    """
    
    @doc """
    Adds two numbers.
    
    ## Examples
    
        iex> MyApp.Math.add(2, 3)
        5
        
        iex> MyApp.Math.add(-1, 1)
        0
    """
    @spec add(number(), number()) :: number()
    def add(a, b), do: a + b
end
  • @moduledoc — module-level documentation
  • @doc — function-level documentation; goes ABOVE def
  • @spec — function type specification (used by Dialyzer)

Markdown in docs

Doc strings are Markdown:

@doc """
Formats a name.

## Options

  * `:upcase` — convert to uppercase
  * `:reverse` — reverse the characters

## Examples

    iex> Format.name("ada", upcase: true)
    "ADA"
    
## See also

  - `String.upcase/1`
  - `String.reverse/1`
"""
def name(s, opts \\ []), do: ...

Generated docs render this as proper HTML with sections, code blocks, links.

Doctests — examples that stay accurate

The ## Examples section's iex> lines are extracted as tests:

# test/my_app/math_test.exs
defmodule MyApp.MathTest do
    use ExUnit.Case
    doctest MyApp.Math       # runs all iex> examples in MyApp.Math
end

The documentation can't drift from reality — broken examples fail the test suite.

Hiding implementation details

@doc false
def internal_helper(...), do: ...

@doc false excludes the function from generated docs. Different from @moduledoc false which hides the entire module.

Type specs and Dialyzer

@spec parse(String.t()) :: {:ok, integer()} | {:error, String.t()}
def parse(s), do: ...
  • @spec declares the type signature
  • String.t() is the standard type alias for strings
  • Tools like Dialyzer use @spec for static analysis

Common types:

  • String.t() / binary() — strings
  • integer(), non_neg_integer(), pos_integer()
  • float(), number()
  • atom(), boolean()
  • list(), [type], [a: integer]
  • map(), %{key: type}
  • tuple(), {type1, type2}
  • keyword(), keyword(value_type)
  • nil, term(), any(), none()

Custom types

defmodule User do
    @type t :: %__MODULE__{
        name: String.t(),
        age: non_neg_integer(),
        email: String.t() | nil
    }
    defstruct [:name, :age, :email]
end

The @type t is conventional for the "main type" exposed by the module. Other modules reference it as User.t().

ex_doc — generated HTML

Add to mix.exs deps:

{:ex_doc, "~> 0.30", only: :dev, runtime: false}

Then:

mix docs

Generates doc/index.html — beautifully formatted, like the official Elixir docs. Standard for any open-source Elixir library.

Hex.pm publishing

Libraries on Hex automatically display generated docs at hexdocs.pm. The combination of @doc + doctest + ex_doc + Hex makes Elixir's documentation story one of the strongest in any language.

Common mistakes

  • No @moduledoc — generated docs look unfinished. Add at least a one-liner.
  • Examples without doctest — they go stale and become misleading.
  • @doc false on something useful — users can't find it but might still depend on it. Either expose properly or move to a private (defp) function.
  • Skipping @spec — Dialyzer can't help; tooling can't suggest types. Add specs to public functions at minimum.
  • Generic types in @specany() for everything defeats the purpose. Be specific.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…