| name | lang-erlang-library-dev |
| description | Erlang-specific library development patterns. Use when creating OTP libraries, designing public APIs with process patterns, configuring rebar3, managing application resources, publishing to Hex, or writing EDoc. Extends meta-library-dev with Erlang/OTP tooling and idioms. |
Erlang Library Development
Erlang-specific patterns for OTP library development. This skill extends meta-library-dev with Erlang/OTP tooling, process-oriented design, and ecosystem practices.
This Skill Extends
meta-library-dev - Foundational library patterns (API design, versioning, testing strategies)
lang-erlang-dev - Core Erlang patterns (processes, OTP behaviors, supervision)
For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first. For foundational Erlang patterns, see lang-erlang-dev.
This Skill Adds
- Erlang tooling: rebar3 configuration, .app.src files, application structure
- Hex publishing: Package metadata, versioning, documentation
- Library idioms: Public API design, behavior callbacks, application supervision
- OTP conventions: Application structure, configuration, resource management
- EDoc: Documentation generation, type specifications, function docs
- Dialyzer: Type analysis, PLT files, type specifications
This Skill Does NOT Cover
- General library patterns - see
meta-library-dev
- Core Erlang/OTP basics - see
lang-erlang-dev
- Web frameworks - see framework-specific skills
- Distributed systems - see
lang-erlang-distributed-dev
- Performance optimization - see
lang-erlang-performance-dev
Quick Reference
| Task | Command/Pattern |
|---|
| New library app | rebar3 new lib <name> |
| New OTP library | rebar3 new app <name> |
| Compile | rebar3 compile |
| Test | rebar3 eunit or rebar3 ct |
| Type check | rebar3 dialyzer |
| Generate docs | rebar3 edoc |
| Publish (dry run) | rebar3 hex publish --dry-run |
| Publish to Hex | rebar3 hex publish |
| Shell with library | rebar3 shell |
| Run tests | rebar3 do eunit, ct |
rebar3 Configuration
Basic rebar.config
{erl_opts, [
debug_info,
warnings_as_errors,
warn_export_all,
warn_unused_import,
warn_untyped_record
]}.
{deps, []}.
{project_plugins, [rebar3_hex, rebar3_ex_doc]}.
{hex, [
{doc, #{provider => ex_doc}}
]}.
{profiles, [
{test, [
{deps, [
{proper, "1.4.0"},
{meck, "0.9.2"}
]},
{erl_opts, [nowarn_export_all]}
]},
{prod, [
{erl_opts, [no_debug_info, warnings_as_errors]}
]}
]}.
{dialyzer, [
{warnings, [
unmatched_returns,
error_handling,
underspecs
]},
{plt_extra_apps, [ssl, crypto]}
]}.
{xref_checks, [
undefined_function_calls,
undefined_functions,
locals_not_used,
deprecated_function_calls,
deprecated_functions
]}.
{cover_enabled, true}.
{cover_opts, [verbose]}.
Application Resource File (.app.src)
{application, mylib,
[{description, "A library for doing X efficiently"},
{vsn, "0.1.0"},
{registered, []},
{applications, [
kernel,
stdlib
]},
{env, []},
{modules, []},
{licenses, ["Apache-2.0"]},
{links, [
{"GitHub", "https://github.com/username/mylib"},
{"Hex", "https://hex.pm/packages/mylib"}
]},
{build_tools, ["rebar3"]},
{files, [
"src",
"include",
"rebar.config",
"rebar.lock",
"README.md",
"LICENSE"
]}
]}.
OTP Application (.app.src with supervisor)
{application, mylib,
[{description, "OTP library with supervision tree"},
{vsn, "0.1.0"},
{registered, [mylib_sup]},
{mod, {mylib_app, []}},
{applications, [
kernel,
stdlib,
sasl
]},
{env, [
{pool_size, 10},
{timeout, 5000}
]},
{modules, []},
{licenses, ["MIT"]},
{links, [{"GitHub", "https://github.com/username/mylib"}]},
{build_tools, ["rebar3"]}
]}.
Project Structure
Simple Library (No Supervision)
mylib/
├── rebar.config
├── rebar.lock
├── README.md
├── LICENSE
├── src/
│ ├── mylib.app.src # Application resource file
│ ├── mylib.erl # Main public API module
│ ├── mylib_parser.erl # Internal module
│ └── mylib_types.hrl # Type definitions
├── include/ # Public header files
│ └── mylib.hrl
├── test/
│ ├── mylib_eunit.erl # EUnit tests
│ └── mylib_SUITE.erl # Common Test suite
├── doc/ # Generated by EDoc
│ └── overview.edoc # Documentation overview
└── priv/ # Static resources
└── templates/
OTP Application Library
mylib/
├── rebar.config
├── src/
│ ├── mylib.app.src
│ ├── mylib_app.erl # Application behavior
│ ├── mylib_sup.erl # Root supervisor
│ ├── mylib.erl # Public API
│ ├── mylib_server.erl # gen_server worker
│ └── mylib_worker.erl # Worker process
├── include/
│ └── mylib.hrl
├── test/
│ ├── mylib_eunit.erl
│ ├── mylib_SUITE.erl
│ └── mylib_prop.erl # PropEr property tests
├── doc/
│ └── overview.edoc
└── priv/
└── config/
└── defaults.config
Public API Design
Module Organization
Single entry point for users:
-module(mylib).
-export([
start/0,
stop/0,
process/1,
process/2,
format/1
]).
%% @doc Start the application
-spec start() -> ok | {error, term()}.
start() ->
application:ensure_all_started(mylib).
%% @doc Stop the application
-spec stop() -> ok.
stop() ->
application:stop(mylib).
%% @doc Process input with default options
-spec process(Input :: binary()) -> {ok, term()} | {error, term()}.
process(Input) ->
process(Input, #{}).
%% @doc Process input with custom options
-spec process(Input :: binary(), Options :: map()) ->
{ok, term()} | {error, term()}.
process(Input, Options) ->
mylib_parser:parse(Input, Options).
Behavior Definition
Define custom behaviors for extensibility:
-module(mylib_handler).
%% Behavior callback definitions
-callback init(Args :: term()) -> {ok, State :: term()} | {error, Reason :: term()}.
-callback handle_event(Event :: term(), State :: term()) ->
{ok, NewState :: term()} | {error, Reason :: term()}.
-callback terminate(Reason :: term(), State :: term()) -> ok.
-optional_callbacks([terminate/2]).
%% Export behavior
-export_type([handler/0]).
-type handler() :: module().
Implement the behavior:
-module(my_custom_handler).
-behaviour(mylib_handler).
-export([init/1, handle_event/2, terminate/2]).
init(Args) ->
{ok, #{args => Args}}.
handle_event(Event, State) ->
io:format("Received: ~p~n", [Event]),
{ok, State}.
terminate(_Reason, _State) ->
ok.
Options and Configuration
Use maps for flexible options:
-module(mylib_config).
-export([parse_options/1, defaults/0]).
-type options() :: #{
timeout => pos_integer(),
retry => boolean(),
max_retries => pos_integer(),
format => json | xml | binary
}.
-export_type([options/0]).
%% @doc Default configuration
-spec defaults() -> options().
defaults() ->
#{
timeout => 5000,
retry => true,
max_retries => 3,
format => json
}.
%% @doc Merge user options with defaults
-spec parse_options(UserOpts :: map()) -> options().
parse_options(UserOpts) ->
maps:merge(defaults(), UserOpts).
Type Specifications and Dialyzer
Complete Type Specs
-module(mylib_types).
%% Exported types
-export_type([
user_id/0,
user/0,
result/0,
error_reason/0
]).
%% Type definitions
-type user_id() :: non_neg_integer().
-type user() :: #{
id := user_id(),
name := binary(),
email := binary(),
created_at := calendar:datetime()
}.
-type error_reason() ::
not_found |
invalid_input |
timeout |
{internal_error, term()}.
-type result() :: {ok, term()} | {error, error_reason()}.
%% Records with types
-record(config, {
timeout :: pos_integer(),
max_connections :: pos_integer(),
handler :: module()
}).
-type config() :: #config{}.
-export_type([config/0]).
Function Specifications
-module(mylib_api).
%% @doc Create a new user
-spec create_user(Name :: binary(), Email :: binary()) ->
{ok, mylib_types:user()} | {error, mylib_types:error_reason()}.
create_user(Name, Email) when is_binary(Name), is_binary(Email) ->
UserId = generate_id(),
User = #{
id => UserId,
name => Name,
email => Email,
created_at => calendar:universal_time()
},
{ok, User}.
%% @doc Find user by ID
-spec find_user(mylib_types:user_id()) ->
{ok, mylib_types:user()} | {error, not_found}.
find_user(UserId) when is_integer(UserId), UserId >= 0 ->
case lookup_user(UserId) of
undefined -> {error, not_found};
User -> {ok, User}
end.
%% Internal function - no spec needed (dialyzer infers)
lookup_user(UserId) ->
ets:lookup(users_table, UserId).
Dialyzer PLT Management
% rebar.config
{dialyzer, [
{warnings, [
unmatched_returns,
error_handling,
underspecs,
unknown
]},
{plt_apps, all_deps},
{plt_extra_apps, [ssl, crypto, public_key]},
{plt_location, local},
{base_plt_location, global}
]}.
EDoc Documentation
Module Documentation
%%% @doc Main API module for MyLib.
%%%
%%% This module provides the primary interface for working with MyLib.
%%% All operations are safe to use from multiple processes concurrently.
%%%
%%% == Quick Start ==
%%%
%%% ```
%%% 1> mylib:start().
%%% ok
%%% 2> {ok, Result} = mylib:process(<<"input">>).
%%% {ok, #{data => <<"processed">>}}
%%% '''
%%%
%%% == Configuration ==
%%%
%%% The application can be configured via application environment:
%%%
%%% ```
%%% {mylib, [
%%% {timeout, 10000},
%%% {pool_size, 20}
%%% ]}
%%% '''
%%%
%%% @end
-module(mylib).
-author("Your Name <your.email@example.com>").
-copyright("2025 Your Name").
-version("0.1.0").
Function Documentation
%%% @doc Process input data with options.
%%%
%%% This function processes the input binary according to the provided
%%% options and returns the result. Processing is done asynchronously
%%% in a worker pool.
%%%
%%% == Options ==
%%%
%%% <ul>
%%% <li>`timeout' - Maximum time in milliseconds (default: 5000)</li>
%%% <li>`format' - Output format: `json' or `binary' (default: json)</li>
%%% <li>`retry' - Whether to retry on failure (default: true)</li>
%%% </ul>
%%%
%%% == Examples ==
%%%
%%% ```
%%% %% Simple processing
%%% {ok, Result} = mylib:process(<<"data">>).
%%%
%%% %% With custom timeout
%%% {ok, Result} = mylib:process(<<"data">>, #{timeout => 10000}).
%%%
%%% %% Binary output format
%%% {ok, Binary} = mylib:process(<<"data">>, #{format => binary}).
%%% '''
%%%
%%% @see process/1
%%% @end
-spec process(Input :: binary(), Options :: map()) ->
{ok, term()} | {error, term()}.
process(Input, Options) ->
% Implementation
ok.
Type Documentation
%%% @type user_id() = non_neg_integer().
%%% Unique identifier for a user.
%%% @type user() = #{
%%% id := user_id(),
%%% name := binary(),
%%% email := binary(),
%%% created_at := calendar:datetime()
%%% }.
%%% User record containing all user information.
%%% @type error_reason() =
%%% not_found |
%%% invalid_input |
%%% timeout |
%%% {internal_error, term()}.
%%% Possible error reasons returned by library functions.
Overview Documentation
Create doc/overview.edoc:
@author Your Name <your.email@example.com>
@copyright 2025 Your Name
@version 0.1.0
@title MyLib - Efficient Data Processing Library
@doc
== Overview ==
MyLib is a high-performance library for processing data in Erlang/OTP
applications. It provides a simple, consistent API while leveraging
OTP principles for reliability and scalability.
== Features ==
<ul>
<li>Concurrent processing with worker pools</li>
<li>Automatic retries and error handling</li>
<li>Multiple output formats</li>
<li>Full type specifications</li>
<li>Comprehensive test coverage</li>
</ul>
== Installation ==
Add to your `rebar.config':
{deps, [
{mylib, "0.1.0"}
]}.
'''
== Quick Start ==
%% Start the application
ok = mylib:start().
%% Process some data
{ok, Result} = mylib:process(<<"input data">>).
%% Stop the application
ok = mylib:stop().
'''
@end
Testing Patterns
EUnit Tests
-module(mylib_eunit).
-include_lib("eunit/include/eunit.hrl").
%%% Setup/Teardown
setup() ->
application:ensure_all_started(mylib).
cleanup(_) ->
application:stop(mylib).
%%% Test Fixtures
mylib_test_() ->
{setup,
fun setup/0,
fun cleanup/1,
[
{"Process valid input", fun test_process_valid/0},
{"Process invalid input", fun test_process_invalid/0},
{"Process with timeout", fun test_process_timeout/0}
]}.
%%% Tests
test_process_valid() ->
Input = <<"valid input">>,
{ok, Result} = mylib:process(Input),
?assertMatch(#{data := _}, Result).
test_process_invalid() ->
Input = <<"">>,
?assertEqual({error, invalid_input}, mylib:process(Input)).
test_process_timeout() ->
Input = <<"data">>,
Options = #{timeout => 1},
?assertMatch({error, timeout}, mylib:process(Input, Options)).
%%% Property-Based Tests
prop_never_crashes_test_() ->
{timeout, 60, fun() ->
?assert(proper:quickcheck(prop_no_crash(), [{numtests, 1000}]))
end}.
prop_no_crash() ->
?FORALL(Input, binary(),
case mylib:process(Input) of
{ok, _} -> true;
{error, _} -> true
end
).
Common Test Suites
-module(mylib_SUITE).
-include_lib("common_test/include/ct.hrl").
%% CT callbacks
-export([all/0, groups/0, init_per_suite/1, end_per_suite/1]).
-export([init_per_testcase/2, end_per_testcase/2]).
%% Test cases
-export([
test_basic_processing/1,
test_concurrent_processing/1,
test_error_handling/1
]).
%%% CT Callbacks
all() ->
[
{group, basic},
{group, concurrent},
{group, errors}
].
groups() ->
[
{basic, [parallel], [test_basic_processing]},
{concurrent, [parallel], [test_concurrent_processing]},
{errors, [sequence], [test_error_handling]}
].
init_per_suite(Config) ->
{ok, _} = application:ensure_all_started(mylib),
Config.
end_per_suite(_Config) ->
application:stop(mylib),
ok.
init_per_testcase(_TestCase, Config) ->
Config.
end_per_testcase(_TestCase, _Config) ->
ok.
%%% Test Cases
test_basic_processing(Config) ->
Input = <<"test data">>,
{ok, Result} = mylib:process(Input),
ct:log("Result: ~p", [Result]),
#{data := Data} = Result,
true = is_binary(Data).
test_concurrent_processing(Config) ->
Inputs = [<<"input", (integer_to_binary(N))/binary>> || N <- lists:seq(1, 100)],
Self = self(),
[spawn(fun() ->
{ok, _} = mylib:process(Input),
Self ! {done, N}
end) || {N, Input} <- lists:enumerate(Inputs)],
receive_n(100).
test_error_handling(Config) ->
{error, invalid_input} = mylib:process(<<>>),
{error, timeout} = mylib:process(<<"data">>, #{timeout => 1}).
%%% Helpers
receive_n(0) -> ok;
receive_n(N) ->
receive
{done, _} -> receive_n(N - 1)
after 5000 ->
ct:fail("Timeout waiting for concurrent operations")
end.
PropEr Property Tests
-module(mylib_prop).
-include_lib("proper/include/proper.hrl").
-include_lib("eunit/include/eunit.hrl").
%%% Generators
user_id() ->
non_neg_integer().
user_name() ->
?LET(Len, range(1, 50),
?LET(Chars, vector(Len, range($a, $z)),
list_to_binary(Chars))).
valid_user() ->
?LET({Id, Name, Email}, {user_id(), user_name(), user_name()},
#{id => Id, name => Name, email => <<Email/binary, "@example.com">>}).
%%% Properties
prop_create_user_returns_user() ->
?FORALL({Name, Email}, {user_name(), user_name()},
begin
{ok, User} = mylib:create_user(Name, Email),
maps:is_key(id, User) andalso
maps:get(name, User) =:= Name andalso
maps:get(email, User) =:= Email
end).
prop_roundtrip_serialization() ->
?FORALL(User, valid_user(),
begin
Serialized = mylib:serialize(User),
{ok, Deserialized} = mylib:deserialize(Serialized),
User =:= Deserialized
end).
%%% EUnit wrapper
properties_test_() ->
{timeout, 120, [
?_assert(proper:quickcheck(prop_create_user_returns_user(), [{numtests, 100}])),
?_assert(proper:quickcheck(prop_roundtrip_serialization(), [{numtests, 100}]))
]}.
Publishing to Hex
Hex Package Configuration
Add to rebar.config:
{project_plugins, [
rebar3_hex,
rebar3_ex_doc
]}.
{hex, [
{doc, #{provider => ex_doc}}
]}.
{ex_doc, [
{source_url, <<"https://github.com/username/mylib">>},
{extras, [<<"README.md">>, <<"LICENSE">>, <<"CHANGELOG.md">>]},
{main, <<"readme">>}
]}.
Pre-publish Checklist
Publishing Commands
rebar3 hex docs
rebar3 hex publish --dry-run
rebar3 hex publish
rebar3 hex publish docs
rebar3 hex revert 0.1.0
rebar3 hex retire mylib 0.1.0 other "Please upgrade to 0.2.0"
Version Retirement Reasons
rebar3 hex retire mylib VERSION other "Custom message"
rebar3 hex retire mylib VERSION security "Security vulnerability found"
rebar3 hex retire mylib VERSION deprecated "Use new-package instead"
rebar3 hex retire mylib VERSION invalid "Invalid release"
rebar3 hex retire mylib VERSION renamed "Package renamed to new-name"
Application Supervision
Application Behavior
-module(mylib_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
case mylib_sup:start_link() of
{ok, Pid} ->
{ok, Pid};
Error ->
Error
end.
stop(_State) ->
ok.
Root Supervisor
-module(mylib_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
SupFlags = #{
strategy => one_for_one,
intensity => 5,
period => 60
},
ChildSpecs = [
#{
id => mylib_server,
start => {mylib_server, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [mylib_server]
},
#{
id => mylib_pool_sup,
start => {mylib_pool_sup, start_link, []},
restart => permanent,
shutdown => infinity,
type => supervisor,
modules => [mylib_pool_sup]
}
],
{ok, {SupFlags, ChildSpecs}}.
Worker Pool Supervisor
-module(mylib_pool_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
PoolSize = application:get_env(mylib, pool_size, 10),
SupFlags = #{
strategy => one_for_one,
intensity => 10,
period => 60
},
ChildSpecs = [
#{
id => {mylib_worker, N},
start => {mylib_worker, start_link, [N]},
restart => permanent,
shutdown => 5000,
type => worker,
modules => [mylib_worker]
}
|| N <- lists:seq(1, PoolSize)
],
{ok, {SupFlags, ChildSpecs}}.
Configuration Management
Application Environment
% src/mylib.app.src
{application, mylib,
[{env, [
{pool_size, 10},
{timeout, 5000},
{retry_count, 3},
{log_level, info}
]}
]}.
Runtime Configuration
-module(mylib_config).
-export([get/1, get/2, set/2]).
%% @doc Get configuration value
-spec get(Key :: atom()) -> term().
get(Key) ->
case application:get_env(mylib, Key) of
{ok, Value} -> Value;
undefined -> error({config_not_found, Key})
end.
%% @doc Get configuration value with default
-spec get(Key :: atom(), Default :: term()) -> term().
get(Key, Default) ->
application:get_env(mylib, Key, Default).
%% @doc Set configuration value at runtime
-spec set(Key :: atom(), Value :: term()) -> ok.
set(Key, Value) ->
application:set_env(mylib, Key, Value).
Config Files (sys.config)
% config/sys.config
[
{mylib, [
{pool_size, 20},
{timeout, 10000},
{log_level, debug}
]},
{sasl, [
{sasl_error_logger, {file, "log/sasl-error.log"}},
{errlog_type, error}
]},
{kernel, [
{logger_level, info}
]}
].
Common Patterns
Singleton Server
-module(mylib_server).
-behaviour(gen_server).
%% API
-export([start_link/0, call/1, cast/1, get_state/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-define(SERVER, ?MODULE).
%%% API
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
call(Request) ->
gen_server:call(?SERVER, Request).
cast(Request) ->
gen_server:cast(?SERVER, Request).
get_state() ->
gen_server:call(?SERVER, get_state).
%%% Callbacks
init([]) ->
State = #{
started_at => erlang:system_time(second),
requests => 0
},
{ok, State}.
handle_call(get_state, _From, State) ->
{reply, State, State};
handle_call(Request, _From, State = #{requests := Count}) ->
Result = process_request(Request),
{reply, Result, State#{requests => Count + 1}}.
handle_cast(_Msg, State) ->
{noreply, State}.
handle_info(_Info, State) ->
{noreply, State}.
terminate(_Reason, _State) ->
ok.
%%% Internal
process_request(Request) ->
{ok, Request}.
Resource Pool Pattern
-module(mylib_pool).
-export([checkout/0, checkin/1, with_resource/1]).
-define(POOL, mylib_resource_pool).
%% @doc Check out a resource from the pool
-spec checkout() -> {ok, pid()} | {error, no_resources}.
checkout() ->
case ets:lookup(?POOL, available) of
[{available, [Pid | Rest]}] ->
ets:insert(?POOL, {available, Rest}),
ets:insert(?POOL, {in_use, Pid}),
{ok, Pid};
_ ->
{error, no_resources}
end.
%% @doc Return a resource to the pool
-spec checkin(pid()) -> ok.
checkin(Pid) ->
ets:delete_object(?POOL, {in_use, Pid}),
[{available, Available}] = ets:lookup(?POOL, available),
ets:insert(?POOL, {available, [Pid | Available]}),
ok.
%% @doc Execute function with a resource
-spec with_resource(fun((pid()) -> term())) -> {ok, term()} | {error, term()}.
with_resource(Fun) ->
case checkout() of
{ok, Resource} ->
try
Result = Fun(Resource),
{ok, Result}
after
checkin(Resource)
end;
Error ->
Error
end.
Anti-Patterns
1. Exposing Internal Processes
% Bad: Exposes internal PIDs
-spec get_worker() -> pid().
get_worker() ->
whereis(mylib_worker).
% Good: Provide functional API
-spec process(Input :: term()) -> {ok, term()} | {error, term()}.
process(Input) ->
mylib_worker:process(Input).
2. Breaking Module Contracts
% Bad: Changing return type in new version
% v0.1.0
-spec parse(binary()) -> map().
% v0.2.0 - BREAKING
-spec parse(binary()) -> {ok, map()} | {error, term()}.
% Good: Add new function, deprecate old
% v0.2.0
-spec parse(binary()) -> map(). % Deprecated
-spec parse_safe(binary()) -> {ok, map()} | {error, term()}.
3. Ignoring Application Lifecycle
% Bad: Starting processes in module
-module(mylib).
start_worker() ->
spawn(fun() -> worker_loop() end).
% Good: Use supervision tree
-module(mylib_sup).
init([]) ->
ChildSpecs = [worker_spec()],
{ok, {sup_flags(), ChildSpecs}}.
4. Missing Type Specs
% Bad: No specs, dialyzer can't verify
process(Input) ->
transform(Input).
% Good: Full specifications
-spec process(Input :: binary()) -> {ok, term()} | {error, atom()}.
process(Input) when is_binary(Input) ->
transform(Input).
References