返回 Skill 列表
extension
分类: 开发与工程无需 API Key

lang-elixir-library-dev

针对Elixir特定库开发模式,涵盖Hex包创建、OTP应用程序设计、Mix配置、ExDoc文档、类型规范、Dialyzer集成以及发布最佳实践。在创建Elixir库、使用OTP原则设计公共API、管理依赖项或向Hex.pm发布时使用。扩展了元库开发与Elixir生态系统工具。

person作者: jakexiaohubgithub

Elixir Library Development

Elixir-specific patterns for library development. This skill extends meta-library-dev with Elixir tooling, OTP design principles, and Hex ecosystem practices.

This Skill Extends

  • meta-library-dev - Foundational library patterns (API design, versioning, testing strategies)
  • lang-elixir-dev - Core Elixir syntax and OTP fundamentals

For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first.

This Skill Adds

  • Elixir tooling: mix.exs configuration, Mix tasks, Hex publishing
  • OTP design: Application structure, supervision trees, GenServer APIs
  • Documentation: ExDoc, @moduledoc, @doc, doctests
  • Type safety: Typespecs, Dialyzer, contracts
  • Hex ecosystem: Dependencies, configuration, package metadata

This Skill Does NOT Cover

  • General library patterns - see meta-library-dev
  • Basic Elixir syntax - see lang-elixir-dev
  • Phoenix web development - see lang-elixir-phoenix-dev
  • Advanced OTP patterns - see lang-elixir-otp-dev
  • Ecto database patterns - see lang-elixir-ecto-dev

Quick Reference

| Task | Command/Pattern | |------|-----------------| | New library | mix new my_lib | | New supervised lib | mix new my_lib --sup | | Build | mix compile | | Test | mix test | | Generate docs | mix docs | | Type check | mix dialyzer | | Format code | mix format | | Publish (dry run) | mix hex.build | | Publish | mix hex.publish | | Audit deps | mix hex.audit |


Mix.exs Structure

Required Fields for Hex Publishing

defmodule MyLib.MixProject do
  use Mix.Project

  @version "0.1.0"
  @source_url "https://github.com/username/my_lib"

  def project do
    [
      app: :my_lib,
      version: @version,
      elixir: "~> 1.14",
      start_permanent: Mix.env() == :prod,
      deps: deps(),

      # Required for Hex
      description: "A brief description of what this library does",
      package: package(),

      # Documentation
      name: "MyLib",
      source_url: @source_url,
      homepage_url: @source_url,
      docs: docs()
    ]
  end

  # Run "mix help compile.app" to learn about applications
  def application do
    [
      extra_applications: [:logger],
      mod: {MyLib.Application, []}
    ]
  end

  defp deps do
    [
      # Documentation
      {:ex_doc, "~> 0.31", only: :dev, runtime: false},

      # Type checking
      {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},

      # Code quality
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false}
    ]
  end

  defp package do
    [
      name: "my_lib",
      files: ~w(lib .formatter.exs mix.exs README* LICENSE* CHANGELOG*),
      licenses: ["Apache-2.0"],
      links: %{
        "GitHub" => @source_url,
        "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md"
      },
      maintainers: ["Your Name"]
    ]
  end

  defp docs do
    [
      main: "MyLib",
      source_ref: "v#{@version}",
      source_url: @source_url,
      extras: ["README.md", "CHANGELOG.md", "LICENSE"],
      groups_for_modules: [
        "Core": [MyLib, MyLib.Config],
        "Utilities": [MyLib.Utils]
      ]
    ]
  end
end

Package Configuration Best Practices

defp package do
  [
    # Package name (defaults to :app name)
    name: "my_lib",

    # Files to include in package
    files: ~w(
      lib
      .formatter.exs
      mix.exs
      README.md
      LICENSE
      CHANGELOG.md
    ),

    # License (required) - use SPDX identifier
    licenses: ["Apache-2.0"],
    # Or multiple: licenses: ["MIT", "Apache-2.0"]

    # Links (at least one required)
    links: %{
      "GitHub" => @source_url,
      "Docs" => "https://hexdocs.pm/my_lib",
      "Changelog" => "#{@source_url}/blob/main/CHANGELOG.md"
    },

    # Maintainers (optional but recommended)
    maintainers: ["Your Name", "Contributor Name"]
  ]
end

OTP Application Structure

Library without Supervision

For pure functional libraries without processes:

# mix.exs
def application do
  [
    extra_applications: [:logger]
    # No mod: {MyLib.Application, []}
  ]
end

Library with Supervision

For libraries managing processes:

# lib/my_lib/application.ex
defmodule MyLib.Application do
  @moduledoc false
  use Application

  @impl true
  def start(_type, _args) do
    children = [
      # Registry for process lookup
      {Registry, keys: :unique, name: MyLib.Registry},

      # DynamicSupervisor for dynamic workers
      {DynamicSupervisor, name: MyLib.DynamicSupervisor, strategy: :one_for_one},

      # Your library's main supervisor
      MyLib.Supervisor
    ]

    opts = [strategy: :one_for_one, name: MyLib.ApplicationSupervisor]
    Supervisor.start_link(children, opts)
  end
end

# lib/my_lib/supervisor.ex
defmodule MyLib.Supervisor do
  use Supervisor

  def start_link(init_arg) do
    Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
  end

  @impl true
  def init(_init_arg) do
    children = [
      {MyLib.Worker, []},
      {MyLib.Cache, []}
    ]

    Supervisor.init(children, strategy: :one_for_one)
  end
end

Optional Application Start

Allow users to start your application manually:

defmodule MyLib do
  @moduledoc """
  MyLib provides functionality for...

  ## Starting the Application

  If you're using MyLib with supervision, add it to your application's
  supervision tree:

      children = [
        MyLib
      ]

  Or start it manually:

      {:ok, _} = MyLib.start_link([])
  """

  def start_link(opts \\ []) do
    MyLib.Supervisor.start_link(opts)
  end

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {__MODULE__, :start_link, [opts]},
      type: :supervisor
    }
  end
end

Public API Design (Elixir-Specific)

Client API Pattern

Separate client and server implementations:

defmodule MyLib.Cache do
  @moduledoc """
  A simple caching server.
  """
  use GenServer

  # Client API

  @doc """
  Starts the cache with the given options.

  ## Options

    * `:name` - The name to register the cache (default: `__MODULE__`)
    * `:ttl` - Time to live in milliseconds (default: 60_000)
  """
  def start_link(opts \\ []) do
    name = Keyword.get(opts, :name, __MODULE__)
    GenServer.start_link(__MODULE__, opts, name: name)
  end

  @doc """
  Stores a value in the cache.
  """
  def put(cache \\ __MODULE__, key, value) do
    GenServer.call(cache, {:put, key, value})
  end

  @doc """
  Retrieves a value from the cache.
  """
  def get(cache \\ __MODULE__, key) do
    GenServer.call(cache, {:get, key})
  end

  @doc """
  Deletes a key from the cache.
  """
  def delete(cache \\ __MODULE__, key) do
    GenServer.cast(cache, {:delete, key})
  end

  # Server Callbacks

  @impl true
  def init(opts) do
    ttl = Keyword.get(opts, :ttl, 60_000)
    {:ok, %{data: %{}, ttl: ttl}}
  end

  @impl true
  def handle_call({:put, key, value}, _from, state) do
    new_data = Map.put(state.data, key, {value, System.monotonic_time(:millisecond)})
    {:reply, :ok, %{state | data: new_data}}
  end

  @impl true
  def handle_call({:get, key}, _from, state) do
    case Map.get(state.data, key) do
      {value, timestamp} ->
        if System.monotonic_time(:millisecond) - timestamp < state.ttl do
          {:reply, {:ok, value}, state}
        else
          {:reply, {:error, :expired}, state}
        end

      nil ->
        {:reply, {:error, :not_found}, state}
    end
  end

  @impl true
  def handle_cast({:delete, key}, state) do
    new_data = Map.delete(state.data, key)
    {:noreply, %{state | data: new_data}}
  end
end

Configuration API

Provide compile-time and runtime configuration:

defmodule MyLib.Config do
  @moduledoc """
  Configuration for MyLib.

  ## Compile-time Configuration

      config :my_lib,
        default_timeout: 5000,
        max_retries: 3

  ## Runtime Configuration

      MyLib.configure(timeout: 10_000)
  """

  @doc """
  Gets a configuration value.
  """
  def get(key, default \\ nil) do
    Application.get_env(:my_lib, key, default)
  end

  @doc """
  Gets all configuration.
  """
  def get_all do
    Application.get_all_env(:my_lib)
  end

  @doc """
  Updates configuration at runtime.
  """
  def put(key, value) do
    Application.put_env(:my_lib, key, value)
  end

  @doc """
  Gets the timeout configuration.
  """
  def timeout do
    get(:default_timeout, 5000)
  end

  @doc """
  Gets the max retries configuration.
  """
  def max_retries do
    get(:max_retries, 3)
  end
end

Builder Pattern with Structs

defmodule MyLib.Query do
  @moduledoc """
  A query builder for MyLib.
  """

  defstruct [:table, :fields, :where, :limit, :offset]

  @type t :: %__MODULE__{
          table: String.t() | nil,
          fields: [atom()] | :all,
          where: keyword(),
          limit: pos_integer() | nil,
          offset: non_neg_integer() | nil
        }

  @doc """
  Creates a new query.
  """
  @spec new() :: t()
  def new do
    %__MODULE__{fields: :all, where: []}
  end

  @doc """
  Sets the table for the query.
  """
  @spec from(t(), String.t()) :: t()
  def from(%__MODULE__{} = query, table) when is_binary(table) do
    %{query | table: table}
  end

  @doc """
  Selects specific fields.
  """
  @spec select(t(), [atom()]) :: t()
  def select(%__MODULE__{} = query, fields) when is_list(fields) do
    %{query | fields: fields}
  end

  @doc """
  Adds a where clause.
  """
  @spec where(t(), keyword()) :: t()
  def where(%__MODULE__{} = query, conditions) when is_list(conditions) do
    %{query | where: query.where ++ conditions}
  end

  @doc """
  Sets the limit.
  """
  @spec limit(t(), pos_integer()) :: t()
  def limit(%__MODULE__{} = query, limit) when is_integer(limit) and limit > 0 do
    %{query | limit: limit}
  end

  @doc """
  Sets the offset.
  """
  @spec offset(t(), non_neg_integer()) :: t()
  def offset(%__MODULE__{} = query, offset) when is_integer(offset) and offset >= 0 do
    %{query | offset: offset}
  end

  @doc """
  Builds and executes the query.
  """
  @spec run(t()) :: {:ok, [map()]} | {:error, term()}
  def run(%__MODULE__{table: nil}), do: {:error, :no_table}
  def run(%__MODULE__{} = query) do
    # Implementation
    {:ok, []}
  end
end

# Usage:
# MyLib.Query.new()
# |> MyLib.Query.from("users")
# |> MyLib.Query.select([:id, :name])
# |> MyLib.Query.where(active: true)
# |> MyLib.Query.limit(10)
# |> MyLib.Query.run()

Behaviour Definition

Define behaviors for extensibility:

defmodule MyLib.Adapter do
  @moduledoc """
  Behaviour for MyLib adapters.
  """

  @type config :: keyword()
  @type result :: {:ok, term()} | {:error, term()}

  @callback init(config) :: {:ok, term()} | {:error, term()}
  @callback execute(query :: term(), state :: term()) :: result()
  @callback close(state :: term()) :: :ok

  @doc """
  Defines a default implementation for init/1.
  """
  defmacro __using__(_opts) do
    quote do
      @behaviour MyLib.Adapter

      @impl true
      def init(config) do
        {:ok, config}
      end

      defoverridable init: 1
    end
  end
end

# Implementation
defmodule MyLib.Adapters.Memory do
  use MyLib.Adapter

  @impl true
  def execute(query, state) do
    # Implementation
    {:ok, []}
  end

  @impl true
  def close(_state) do
    :ok
  end
end

Typespecs and Dialyzer

Basic Typespecs

defmodule MyLib.Parser do
  @moduledoc """
  Parses data in various formats.
  """

  @type input :: String.t() | binary()
  @type parse_error :: {:error, :invalid_format | :empty_input}
  @type parse_result :: {:ok, map()} | parse_error()

  @doc """
  Parses the given input.
  """
  @spec parse(input()) :: parse_result()
  def parse(""), do: {:error, :empty_input}
  def parse(input) when is_binary(input) do
    # Implementation
    {:ok, %{}}
  end

  @spec parse!(input()) :: map()
  def parse!(input) do
    case parse(input) do
      {:ok, result} -> result
      {:error, reason} -> raise ArgumentError, "Parse failed: #{reason}"
    end
  end
end

Advanced Type Definitions

defmodule MyLib.Types do
  @moduledoc """
  Type definitions for MyLib.
  """

  # Opaque types (hide implementation)
  @opaque token :: {String.t(), pos_integer()}

  # Custom types
  @type user_id :: pos_integer()
  @type username :: String.t()
  @type user :: %{id: user_id(), name: username(), email: String.t()}

  # Union types
  @type result(success, error) :: {:ok, success} | {:error, error}

  # Generic types
  @type collection(item) :: [item] | MapSet.t(item)

  # Complex structs
  @typedoc """
  Configuration for MyLib operations.
  """
  @type config :: %{
          required(:timeout) => pos_integer(),
          required(:retries) => non_neg_integer(),
          optional(:debug) => boolean(),
          optional(:adapter) => module()
        }
end

Dialyzer Configuration

# mix.exs
def project do
  [
    # ...
    dialyzer: dialyzer()
  ]
end

defp dialyzer do
  [
    plt_file: {:no_warn, "priv/plts/dialyzer.plt"},
    plt_add_apps: [:ex_unit, :mix],
    flags: [
      :error_handling,
      :underspecs,
      :unmatched_returns
    ],
    # Paths to check
    paths: ["_build/#{Mix.env()}/lib/my_lib/ebin"],
    # Ignore specific warnings
    ignore_warnings: ".dialyzer_ignore.exs"
  ]
end

.dialyzer_ignore.exs:

[
  # Ignore specific warnings
  {"lib/my_lib/legacy.ex", :no_return},
  ~r/.*vendored.*/
]

ExDoc Documentation

Module Documentation

defmodule MyLib do
  @moduledoc """
  MyLib provides functionality for...

  ## Installation

  Add `my_lib` to your list of dependencies in `mix.exs`:

      def deps do
        [
          {:my_lib, "~> 0.1.0"}
        ]
      end

  ## Usage

      iex> MyLib.parse("data")
      {:ok, %{key: "value"}}

  ## Configuration

      config :my_lib,
        timeout: 5000,
        max_retries: 3

  ## Examples

  ### Basic Usage

      MyLib.Query.new()
      |> MyLib.Query.from("users")
      |> MyLib.Query.run()

  ### Advanced Usage

      query =
        MyLib.Query.new()
        |> MyLib.Query.from("orders")
        |> MyLib.Query.where(status: "pending")
        |> MyLib.Query.limit(100)

      case MyLib.Query.run(query) do
        {:ok, results} -> process_results(results)
        {:error, reason} -> handle_error(reason)
      end
  """

  @doc """
  Parses the given input data.

  ## Parameters

    * `input` - The input string to parse
    * `opts` - Optional keyword list of options:
      * `:strict` - Enable strict parsing (default: `false`)
      * `:format` - Output format (default: `:map`)

  ## Returns

    * `{:ok, result}` - Successfully parsed data
    * `{:error, reason}` - Parse error with reason

  ## Examples

      iex> MyLib.parse("key=value")
      {:ok, %{"key" => "value"}}

      iex> MyLib.parse("invalid", strict: true)
      {:error, :invalid_format}

      iex> MyLib.parse("a=1&b=2", format: :keyword)
      {:ok, [a: "1", b: "2"]}

  ## Error Reasons

    * `:invalid_format` - Input doesn't match expected format
    * `:empty_input` - Input is empty or nil
    * `:parse_error` - Generic parsing error
  """
  @spec parse(String.t(), keyword()) :: {:ok, map()} | {:error, atom()}
  def parse(input, opts \\ []) do
    # Implementation
  end

  @doc """
  Same as `parse/2` but raises on error.

  ## Examples

      iex> MyLib.parse!("key=value")
      %{"key" => "value"}

      iex> MyLib.parse!("invalid")
      ** (ArgumentError) invalid input
  """
  @spec parse!(String.t(), keyword()) :: map()
  def parse!(input, opts \\ []) do
    case parse(input, opts) do
      {:ok, result} -> result
      {:error, reason} -> raise ArgumentError, "parse failed: #{reason}"
    end
  end

  @doc since: "0.2.0"
  @doc deprecated: "Use parse/2 instead"
  def old_parse(input) do
    parse(input)
  end

  @doc false
  def internal_helper do
    # This won't appear in docs
  end
end

Documentation Groups

# mix.exs
defp docs do
  [
    main: "MyLib",
    logo: "assets/logo.png",
    source_ref: "v#{@version}",
    source_url: @source_url,
    extras: [
      "README.md",
      "CHANGELOG.md",
      "guides/getting_started.md",
      "guides/advanced_usage.md"
    ],
    groups_for_extras: [
      Guides: ~r/guides\/.*/
    ],
    groups_for_modules: [
      Core: [MyLib, MyLib.Config],
      Adapters: [MyLib.Adapter, MyLib.Adapters.Memory],
      Utilities: [MyLib.Utils, MyLib.Helpers]
    ],
    groups_for_functions: [
      "CRUD Operations": &(&1[:section] == :crud),
      "Query Building": &(&1[:section] == :query)
    ]
  ]
end

Doctests

defmodule MyLib.Math do
  @doc """
  Adds two numbers.

  ## Examples

      iex> MyLib.Math.add(1, 2)
      3

      iex> MyLib.Math.add(-1, 1)
      0

  Multiple lines:

      iex> result = MyLib.Math.add(10, 20)
      iex> result * 2
      60

  Pattern matching:

      iex> {:ok, result} = {:ok, MyLib.Math.add(5, 5)}
      iex> result
      10

  Exceptions:

      iex> MyLib.Math.add("a", "b")
      ** (ArithmeticError) bad argument in arithmetic expression
  """
  def add(a, b) do
    a + b
  end
end

Run doctests:

# test/my_lib_test.exs
defmodule MyLibTest do
  use ExUnit.Case
  doctest MyLib
  doctest MyLib.Math
end

Testing Patterns

Unit Tests

defmodule MyLib.ParserTest do
  use ExUnit.Case, async: true

  alias MyLib.Parser

  describe "parse/1" do
    test "parses valid input" do
      assert {:ok, %{"key" => "value"}} = Parser.parse("key=value")
    end

    test "returns error for invalid input" do
      assert {:error, :invalid_format} = Parser.parse("invalid")
    end

    test "handles empty input" do
      assert {:error, :empty_input} = Parser.parse("")
    end
  end

  describe "parse!/1" do
    test "returns parsed data on success" do
      assert %{"key" => "value"} = Parser.parse!("key=value")
    end

    test "raises on error" do
      assert_raise ArgumentError, fn ->
        Parser.parse!("invalid")
      end
    end
  end
end

GenServer Testing

defmodule MyLib.CacheTest do
  use ExUnit.Case, async: true

  alias MyLib.Cache

  setup do
    # Start cache for this test
    {:ok, cache} = start_supervised({Cache, name: :"cache_#{:rand.uniform(10000)}"})
    %{cache: cache}
  end

  test "stores and retrieves values", %{cache: cache} do
    assert :ok = Cache.put(cache, :key, "value")
    assert {:ok, "value"} = Cache.get(cache, :key)
  end

  test "returns error for missing keys", %{cache: cache} do
    assert {:error, :not_found} = Cache.get(cache, :missing)
  end

  test "expires old values", %{cache: cache} do
    {:ok, cache_with_ttl} = start_supervised({Cache, ttl: 100})

    Cache.put(cache_with_ttl, :key, "value")
    assert {:ok, "value"} = Cache.get(cache_with_ttl, :key)

    Process.sleep(150)
    assert {:error, :expired} = Cache.get(cache_with_ttl, :key)
  end
end

Property-Based Testing

defmodule MyLib.PropertyTest do
  use ExUnit.Case
  use ExUnitProperties

  alias MyLib.Parser

  property "parse never crashes" do
    check all input <- string(:printable) do
      # Should never raise
      case Parser.parse(input) do
        {:ok, _} -> :ok
        {:error, _} -> :ok
      end
    end
  end

  property "parse roundtrip" do
    check all data <- map_of(atom(:alphanumeric), string(:alphanumeric)) do
      serialized = MyLib.serialize(data)
      {:ok, parsed} = Parser.parse(serialized)
      assert parsed == data
    end
  end
end

Integration Tests

defmodule MyLib.IntegrationTest do
  use ExUnit.Case

  @moduletag :integration

  setup_all do
    # Start application
    {:ok, _} = Application.ensure_all_started(:my_lib)
    on_exit(fn -> Application.stop(:my_lib) end)
    :ok
  end

  test "end-to-end workflow" do
    # Start a process
    {:ok, pid} = MyLib.Worker.start_link([])

    # Perform operations
    assert :ok = MyLib.Worker.put(pid, :key, "value")
    assert {:ok, "value"} = MyLib.Worker.get(pid, :key)

    # Cleanup
    GenServer.stop(pid)
  end
end

Dependency Management

Hex Dependencies

defp deps do
  [
    # Production dependencies
    {:jason, "~> 1.4"},
    {:plug, "~> 1.15"},

    # Optional dependencies
    {:telemetry, "~> 1.2", optional: true},

    # Development only
    {:ex_doc, "~> 0.31", only: :dev, runtime: false},
    {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
    {:credo, "~> 1.7", only: [:dev, :test], runtime: false},

    # Test only
    {:stream_data, "~> 1.0", only: :test},
    {:mox, "~> 1.1", only: :test}
  ]
end

Version Constraints

| Constraint | Meaning | Example | |------------|---------|---------| | ~> 1.4 | >= 1.4.0 and < 2.0.0 | Recommended | | ~> 1.4.5 | >= 1.4.5 and < 1.5.0 | Patch updates only | | >= 1.4.0 | Any version >= 1.4.0 | Too permissive | | == 1.4.0 | Exactly 1.4.0 | Too restrictive |

Optional Dependencies

# In mix.exs
defp deps do
  [
    {:jason, "~> 1.4", optional: true}
  ]
end

# In code
if Code.ensure_loaded?(Jason) do
  def encode(data), do: Jason.encode(data)
else
  def encode(_data), do: {:error, :jason_not_available}
end

Publishing to Hex

Pre-publish Checklist

  • [ ] mix compile --warnings-as-errors passes
  • [ ] mix test passes all tests
  • [ ] mix format --check-formatted passes
  • [ ] mix credo --strict passes (if using Credo)
  • [ ] mix dialyzer passes (if using Dialyzer)
  • [ ] mix docs generates correctly
  • [ ] Version bumped in mix.exs
  • [ ] CHANGELOG.md updated
  • [ ] README.md is current
  • [ ] All required package fields in mix.exs
  • [ ] License file present
  • [ ] mix hex.build succeeds

Publishing Commands

# Build package locally
mix hex.build

# Verify package contents
unzip my_lib-0.1.0.tar -d /tmp/package
ls -la /tmp/package

# Publish to Hex (requires authentication)
mix hex.publish

# Publish docs separately (if needed)
mix hex.publish docs

# Revert a version (within 24 hours)
mix hex.publish --revert 0.1.0

Hex Authentication

# Authenticate with Hex
mix hex.user auth

# Register new user
mix hex.user register

# Create API key
mix hex.user key generate

Package Retirement

# Retire a version (discourage use)
mix hex.retire my_lib 0.1.0 "Security vulnerability"

# Unretire
mix hex.retire my_lib 0.1.0 --unretire

Module Organization

Standard Library Structure

my_lib/
├── lib/
│   ├── my_lib.ex              # Main public API
│   ├── my_lib/
│   │   ├── application.ex     # OTP application
│   │   ├── supervisor.ex      # Main supervisor
│   │   ├── config.ex          # Configuration API
│   │   ├── parser.ex          # Core functionality
│   │   ├── types.ex           # Type definitions
│   │   ├── adapters/          # Adapter modules
│   │   │   ├── adapter.ex     # Behaviour definition
│   │   │   └── memory.ex      # Implementation
│   │   └── internal/          # Private modules
│   │       └── utils.ex
│   └── mix/
│       └── tasks/             # Custom Mix tasks
│           └── my_lib.gen.ex
├── test/
│   ├── my_lib_test.exs
│   ├── my_lib/
│   │   ├── parser_test.exs
│   │   └── adapters/
│   │       └── memory_test.exs
│   ├── support/
│   │   └── test_helpers.ex
│   └── test_helper.exs
├── priv/                      # Private runtime files
│   └── templates/
├── .formatter.exs
├── mix.exs
├── README.md
├── CHANGELOG.md
└── LICENSE

Main Module Pattern

defmodule MyLib do
  @moduledoc """
  Main entry point for MyLib.
  """

  # Re-export public API
  defdelegate parse(input), to: MyLib.Parser
  defdelegate parse(input, opts), to: MyLib.Parser
  defdelegate serialize(data), to: MyLib.Serializer

  # Direct implementations
  def version, do: Application.spec(:my_lib, :vsn) |> to_string()

  def child_spec(opts) do
    %{
      id: __MODULE__,
      start: {MyLib.Supervisor, :start_link, [opts]},
      type: :supervisor
    }
  end
end

Anti-Patterns

1. Process Dictionary Abuse

# Bad: Using process dictionary in libraries
def get_config do
  Process.get(:my_lib_config)
end

# Good: Explicit state passing
def get_config(%State{config: config}), do: config

2. Global Named Processes

# Bad: Single global process
GenServer.start_link(__MODULE__, [], name: __MODULE__)

# Good: Allow multiple instances
GenServer.start_link(__MODULE__, [], name: name)

3. Missing Supervision

# Bad: Spawning unsupervised processes
spawn(fn -> do_work() end)

# Good: Use Task.Supervisor or DynamicSupervisor
Task.Supervisor.start_child(MyLib.TaskSupervisor, fn -> do_work() end)

4. Breaking API Changes

# v0.1.0
def fetch(id), do: {:ok, data}

# v0.2.0 - WRONG! Breaking change in minor version
def fetch(id, opts \\ []), do: {:ok, data}

# v0.2.0 - Correct: Add new function
def fetch(id), do: fetch(id, [])
def fetch_with_opts(id, opts), do: {:ok, data}

5. Missing Typespecs

# Bad: No typespec
def process(data, opts) do
  # ...
end

# Good: Full typespec
@spec process(map(), keyword()) :: {:ok, result()} | {:error, term()}
def process(data, opts) do
  # ...
end

Common Mix Tasks

Custom Mix Tasks

# lib/mix/tasks/my_lib.gen.ex
defmodule Mix.Tasks.MyLib.Gen do
  use Mix.Task

  @shortdoc "Generates MyLib configuration"
  @moduledoc """
  Generates MyLib configuration file.

      mix my_lib.gen

  ## Options

    * `--path` - Output path (default: config/my_lib.exs)
  """

  @impl Mix.Task
  def run(args) do
    {opts, _, _} = OptionParser.parse(args, switches: [path: :string])
    path = opts[:path] || "config/my_lib.exs"

    File.write!(path, """
    import Config

    config :my_lib,
      timeout: 5000,
      max_retries: 3
    """)

    Mix.shell().info("Generated #{path}")
  end
end

References