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

Elixir Writing Docs

by @anderskev

Guides writing Elixir documentation with @moduledoc, @doc, @typedoc, doctests, cross-references, and metadata. Use when adding or improving documentation in...

Versionv1.2.1
Downloads461
Installs1
TERMINAL
clawhub install elixir-writing-docs

πŸ“– About This Skill


name: elixir-writing-docs description: Guides writing Elixir documentation with @moduledoc, @doc, @typedoc, doctests, cross-references, and metadata. Use when adding or improving documentation in .ex files.

Elixir Writing Docs

Quick Reference

| Topic | Reference | |-------|-----------| | Doctests: syntax, gotchas, when to use | references/doctests.md | | Cross-references and linking syntax | references/cross-references.md | | Admonitions, formatting, tabs | references/admonitions-and-formatting.md |

First-Line Summary Rule

ExDoc and tools like mix docs extract the first paragraph of @moduledoc and @doc as a summary. Keep the opening line concise and self-contained.

# GOOD - first line works as a standalone summary
@moduledoc """
Handles payment processing through Stripe and local ledger reconciliation.

Wraps the Stripe API client and ensures each charge is recorded in the local ledger before returning a confirmation to the caller. """

BAD - first line is vague, forces reader to continue

@moduledoc """ This module contains various functions related to payments.

It uses Stripe and also updates the ledger. """

The same rule applies to @doc:

# GOOD
@doc """
Charges a customer's default payment method for the given amount in cents.

Returns {:ok, charge} on success or {:error, reason} when the payment gateway rejects the request. """

BAD

@doc """ This function is used to charge a customer. """

@moduledoc Structure

A well-structured @moduledoc follows this pattern:

defmodule MyApp.Inventory do
  @moduledoc """
  Tracks warehouse stock levels and triggers replenishment orders.

This module maintains an ETS-backed cache of current quantities and exposes functions for atomic stock adjustments. It is designed to be started under a supervisor and will restore state from the database on init.

## Examples

iex> {:ok, pid} = MyApp.Inventory.start_link(warehouse: :east) iex> MyApp.Inventory.current_stock(pid, "SKU-1042") {:ok, 350}

## Configuration

Expects the following in config/runtime.exs:

config :my_app, MyApp.Inventory, repo: MyApp.Repo, low_stock_threshold: 50 """ end

Key points:

  • First paragraph is the summary (one to two sentences).
  • ## Examples shows realistic usage. Use doctests when the example is runnable.
  • ## Configuration documents required config keys. Omit this section if the module takes no config.
  • Use second-level headings (##) only. First-level (#) is reserved for the module name in ExDoc output.
  • Documenting Behaviour Modules

    When defining a behaviour, document the expected callbacks:

    defmodule MyApp.PaymentGateway do
      @moduledoc """
      Behaviour for payment gateway integrations.

    Implementations must handle charging, refunding, and status checks. See MyApp.PaymentGateway.Stripe for a reference implementation.

    ## Callbacks

    * charge/2 - Initiate a charge for a given amount * refund/2 - Refund a previously completed charge * status/1 - Check the status of a transaction """

    @callback charge(amount :: pos_integer(), currency :: atom()) :: {:ok, transaction_id :: String.t()} | {:error, term()}

    @callback refund(transaction_id :: String.t(), amount :: pos_integer()) :: :ok | {:error, term()}

    @callback status(transaction_id :: String.t()) :: {:pending | :completed | :failed, map()} end

    @doc Structure

    @doc """
    Reserves the given quantity of an item, decrementing available stock.

    Returns {:ok, reservation_id} when stock is available, or {:error, :insufficient_stock} when the requested quantity exceeds what is on hand.

    Examples

    iex> MyApp.Inventory.reserve("SKU-1042", 5) {:ok, "res_abc123"}

    iex> MyApp.Inventory.reserve("SKU-9999", 1) {:error, :not_found}

    Options

    * :warehouse - Target warehouse atom. Defaults to :primary. * :timeout - Timeout in milliseconds. Defaults to 5_000. """ @spec reserve(String.t(), pos_integer(), keyword()) :: {:ok, String.t()} | {:error, :insufficient_stock | :not_found} def reserve(sku, quantity, opts \\ []) do # ... end

    Guidelines:

  • State what the function does, then what it returns.
  • Document each option in a bulleted ## Options section when the function accepts a keyword list.
  • Place @spec between @doc and def. This is the conventional ordering.
  • Include doctests for pure functions. Skip them for side-effecting functions (see references/doctests.md).
  • @typedoc

    Document custom types defined with @type or @opaque:

    @typedoc """
    A positive integer representing an amount in the smallest currency unit (e.g., cents).
    """
    @type amount :: pos_integer()

    @typedoc """ Reservation status returned by status/1.

    * :held - Stock is reserved but not yet shipped * :released - Reservation was cancelled and stock restored * :fulfilled - Items have shipped """ @type reservation_status :: :held | :released | :fulfilled

    @typedoc """ Opaque handle returned by connect/1. Do not pattern-match on this value. """ @opaque connection :: %__MODULE__{socket: port(), buffer: binary()}

    For @opaque types, the @typedoc is especially important because callers cannot inspect the structure.

    Metadata

    @doc since and @doc deprecated

    @doc since: "1.3.0"
    @doc """
    Transfers stock between two warehouses.
    """
    def transfer(from, to, sku, quantity), do: # ...

    @doc deprecated: "Use transfer/4 instead" @doc """ Moves items between locations. Deprecated in favor of transfer/4 which supports cross-region transfers. """ def move_stock(from, to, sku, quantity), do: # ...

    You can combine metadata and the docstring in one attribute:

    @doc since: "2.0.0", deprecated: "Use bulk_reserve/2 instead"
    @doc """
    Reserves multiple items in a single call.
    """
    def batch_reserve(items), do: # ...
    

    @moduledoc since: works the same way for modules:

    @moduledoc since: "1.2.0"
    @moduledoc """
    Handles webhook signature verification for Stripe events.
    """
    

    When to Use @doc false / @moduledoc false

    Suppress documentation when the module or function is not part of the public API:

    # Private implementation module β€” internal to the application
    defmodule MyApp.Inventory.StockCache do
      @moduledoc false
      # ...
    end

    Protocol implementation β€” documented at the protocol level

    defimpl String.Chars, for: MyApp.Money do @moduledoc false # ... end

    Callback implementation β€” documented at the behaviour level

    @doc false def handle_info(:refresh, state) do # ... end

    Helper used only inside the module

    @doc false def do_format(value), do: # ...

    Do NOT use @doc false on genuinely public functions. If a function is exported and callers depend on it, document it. If it should not be called externally, make it private with defp.

    Documentation vs Code Comments

    | | Documentation (@moduledoc, @doc) | Code Comments (#) | |---|---|---| | Audience | Users of your API | Developers reading source | | Purpose | Contract: what it does, what it returns | Why a particular implementation choice was made | | Rendered | Yes, by ExDoc in HTML/epub | No, visible only in source | | Required | All public modules and functions | Only where code intent is non-obvious |

    @doc """
    Validates that the given coupon code is active and has remaining uses.
    """
    @spec validate_coupon(String.t()) :: {:ok, Coupon.t()} | {:error, :expired | :exhausted}
    def validate_coupon(code) do
      # We query the read replica here to avoid adding load to the
      # primary during high-traffic discount events.
      Repo.replica().get_by(Coupon, code: code)
      |> check_expiry()
      |> check_remaining_uses()
    end
    

    The @doc tells the caller what validate_coupon/1 does and returns. The inline comment explains an implementation decision that would otherwise be surprising.

    Completing documentation (gates)

    Finish with these sequenced checks. Skip a step when it does not apply.

    1. Doctests added or changed? Run the project’s doctest verification (usually mix test for affected modules or the full suite). Pass: no doctest failures. 2. Cross-references, backticks, or m: links added or edited? Run mix docs. Pass: the command completes; resolve ExDoc warnings about missing modules, callbacks, or bad links. 3. New or changed public API? Pass: every exported def / defmacro has an intentional @doc or @doc false, and every public module has @moduledoc or @moduledoc false, consistent with your project’s policy.

    When to Load References

  • Writing doctests or debugging doctest failures --> references/doctests.md
  • Adding links between modules, functions, types --> references/cross-references.md
  • Using admonition blocks, tabs, or formatting in docs --> references/admonitions-and-formatting.md
  • πŸ’‘ Examples

    iex> {:ok, pid} = MyApp.Inventory.start_link(warehouse: :east) iex> MyApp.Inventory.current_stock(pid, "SKU-1042") {:ok, 350}

    ## Configuration

    Expects the following in config/runtime.exs:

    config :my_app, MyApp.Inventory, repo: MyApp.Repo, low_stock_threshold: 50 """ end

    
    Key points:

  • First paragraph is the summary (one to two sentences).
  • ## Examples shows realistic usage. Use doctests when the example is runnable.
  • ## Configuration documents required config keys. Omit this section if the module takes no config.
  • Use second-level headings (##) only. First-level (#) is reserved for the module name in ExDoc output.
  • Documenting Behaviour Modules

    When defining a behaviour, document the expected callbacks:

    elixir defmodule MyApp.PaymentGateway do @moduledoc """ Behaviour for payment gateway integrations.

    Implementations must handle charging, refunding, and status checks. See MyApp.PaymentGateway.Stripe for a reference implementation.

    ## Callbacks

    * charge/2 - Initiate a charge for a given amount * refund/2 - Refund a previously completed charge * status/1 - Check the status of a transaction """

    @callback charge(amount :: pos_integer(), currency :: atom()) :: {:ok, transaction_id :: String.t()} | {:error, term()}

    @callback refund(transaction_id :: String.t(), amount :: pos_integer()) :: :ok | {:error, term()}

    @callback status(transaction_id :: String.t()) :: {:pending | :completed | :failed, map()} end ```

    βš™οΈ Configuration

    Expects the following in config/runtime.exs:

    config :my_app, MyApp.Inventory, repo: MyApp.Repo, low_stock_threshold: 50 """ end

    
    Key points:

  • First paragraph is the summary (one to two sentences).
  • ## Examples shows realistic usage. Use doctests when the example is runnable.
  • ## Configuration documents required config keys. Omit this section if the module takes no config.
  • Use second-level headings (##) only. First-level (#) is reserved for the module name in ExDoc output.
  • Documenting Behaviour Modules

    When defining a behaviour, document the expected callbacks:

    elixir defmodule MyApp.PaymentGateway do @moduledoc """ Behaviour for payment gateway integrations.

    Implementations must handle charging, refunding, and status checks. See MyApp.PaymentGateway.Stripe for a reference implementation.

    ## Callbacks

    * charge/2 - Initiate a charge for a given amount * refund/2 - Refund a previously completed charge * status/1 - Check the status of a transaction """

    @callback charge(amount :: pos_integer(), currency :: atom()) :: {:ok, transaction_id :: String.t()} | {:error, term()}

    @callback refund(transaction_id :: String.t(), amount :: pos_integer()) :: :ok | {:error, term()}

    @callback status(transaction_id :: String.t()) :: {:pending | :completed | :failed, map()} end ```