Curated library of 38 atomic skills, 7 personas, and 1 orchestrator for Elixir and Phoenix development. Organized by category: fundamentals, phoenix, database, testing, auth, infrastructure, quality, security, integrations, tooling, frameworks, personas, and orchestration. Covers core Elixir patterns, Phoenix LiveView, Ecto, OTP, Oban, testing, security, deployment, real-time, and modern tooling (Req, Swoosh, Cachex, Broadway, Ash).
91
91%
Does it follow best practices?
Impact
91%
1.37xAverage score across 56 eval scenarios
Advisory
Suggest reviewing before use
Sections: RULES · End-to-End Workflow · Quick-Reference: Request Types · Retries · Streaming Responses · Common Pitfalls · Integration
Req.new/1 — set base_url, receive_timeout, and default headers once, then reuse it for every call instead of re-passing optionsReq.get/1 / Req.post/1 in application code — pattern match {:ok, %{status: _, body: _}} / {:error, _}; reserve the ! variants for scripts and tests404, 429, and status >= 500 distinctly; never collapse every non-200 into one branchretry: :transient only for idempotent requests — Req retries 5xx and network errors with backoff; never blindly retry non-idempotent writesreceive_timeout — never rely on infinite defaults for calls to external servicesReq.Test — the suite must never hit a real APIinto: — write to File.stream!/1 or a callback instead of loading the full payload into memorySee assets/req_client_snippets.ex for a copy-paste base client and wrapper module.
Follow this sequence when integrating an external API:
Step 1 — Add dependency
# mix.exs
defp deps do
[
{:req, "~> 0.5"}
]
endCheckpoint: run mix deps.get and confirm Req compiles without errors.
Step 2 — Create a configured client module
defmodule MyApp.ApiClient do
def base_request do
Req.new(
base_url: Application.get_env(:my_app, :api_base_url),
headers: [{"authorization", "Bearer #{api_token()}"}],
receive_timeout: 30_000,
retry: :transient
)
end
def fetch_user(id) do
case Req.get(base_request(), url: "/users/#{id}") do
{:ok, %{status: 200, body: body}} -> {:ok, body}
{:ok, %{status: 404}} -> {:error, :not_found}
{:ok, %{status: 429}} -> {:error, :rate_limited} # extend with additional codes as needed
{:ok, %{status: status}} when status >= 500 -> {:error, :server_error}
{:ok, %{status: status}} -> {:error, {:unexpected_status, status}}
{:error, %Mint.TransportError{reason: :timeout}} -> {:error, :timeout}
{:error, exception} -> {:error, Exception.message(exception)}
end
end
defp api_token, do: Application.get_env(:my_app, :api_token)
endCheckpoint: verify the module compiles with mix compile.
Step 3 — Test with Req.Test before touching a real API
defmodule MyApp.ApiClientTest do
use ExUnit.Case, async: true
setup do
Req.Test.adapter(MyApp.ApiClient)
:ok
end
test "fetches user successfully" do
Req.Test.stub(MyApp.ApiClient, fn conn ->
Req.Test.json(conn, %{"id" => 1, "name" => "John"})
end)
assert {:ok, %{"name" => "John"}} = MyApp.ApiClient.fetch_user(1)
end
test "handles not found" do
Req.Test.stub(MyApp.ApiClient, fn conn ->
conn |> Plug.Conn.put_status(404) |> Req.Test.json(%{"error" => "not found"})
end)
assert {:error, :not_found} = MyApp.ApiClient.fetch_user(999)
end
endCheckpoint: run mix test — all stubs must pass before using the real API.
Step 4 — Verify in IEx against the real endpoint
iex> MyApp.ApiClient.fetch_user(1)
{:ok, %{"id" => 1, "name" => "John", ...}}Checkpoint: confirm a {:ok, body} tuple is returned; check logs for retry warnings if the request is slow.
| Pattern | Example |
|---|---|
| GET | Req.get!(url, params: %{page: 1}) |
| POST JSON | Req.post!(url, json: %{name: "John"}) |
| POST form | Req.post!(url, form: [username: "john", password: "secret"]) |
| With error handling | Use Req.get/1 (not bang) and pattern match {:ok, %{status: _, body: _}} / {:error, _} |
# Automatic retries for transient failures
Req.get!("https://api.example.com/data",
retry: :transient, # Retry on 5xx and network errors
retry_delay: &(&1 * 1000), # Exponential backoff: 1s, 2s, 4s, ...
max_retries: 3, # Max 3 retries
retry_log_level: :warning
)
# Custom retry logic (e.g. also retry on 429)
Req.get!("https://api.example.com/data",
retry: fn response ->
case response do
%{status: 429} -> true
%{status: s} when s >= 500 -> true
_ -> false
end
end,
max_retries: 3
)Prefer into: over loading the full response into memory — this is Req's built-in
streaming support, and it's the right tool whenever a response body could be large or
unbounded:
# Stream large responses to a file
Req.get!("https://api.example.com/large-file",
into: File.stream!("download.txt")
)
# Stream with a callback
Req.get!("https://api.example.com/stream",
into: fn {:data, data}, {req, resp} ->
IO.puts("Received #{byte_size(data)} bytes")
{:cont, {req, resp}}
end
)| ❌ Don't | ✅ Do |
|---|---|
Req.get! in app code and rescue exceptions | Req.get/1 and pattern match {:ok, _} / {:error, _} |
Rebuild Req.new/1 options on every call | Build a base client once and reuse it |
| Treat any non-200 status the same | Match 404, 429, and status >= 500 explicitly |
retry: :transient on non-idempotent POSTs | Retry only idempotent requests; handle writes deliberately |
Leave receive_timeout at the default | Set an explicit timeout for every external call |
| Hit the real API in the test suite | Stub with Req.Test.stub/2 |
| Load a large response into memory | Stream with into: File.stream!(path) |
| Hardcode tokens in the client module | Read from Application.get_env/2 / runtime config |
| Predecessor | This Skill | Successor |
|---|---|---|
| elixir-essentials | req-http-client | testing-essentials |
| None (standalone) | req-http-client | oban-essentials |
Companion skills:
testing-essentials — stub HTTP calls with Req.Test in the suiteoban-essentials — retry and schedule outbound API calls in background jobscachex-caching — cache responses from slow or rate-limited APIs.tessl-plugin
evals
scenario-1
scenario-2
scenario-3
scenario-4
scenario-5
scenario-6
scenario-7
scenario-8
scenario-9
scenario-10
scenario-11
scenario-12
scenario-13
scenario-14
scenario-15
scenario-16
scenario-17
scenario-18
scenario-19
scenario-20
scenario-21
scenario-22
scenario-23
scenario-24
scenario-25
scenario-26
scenario-27
scenario-28
scenario-29
scenario-30
scenario-31
scenario-32
scenario-33
scenario-34
scenario-35
scenario-36
scenario-37
scenario-38
scenario-39
scenario-40
scenario-41
scenario-42
scenario-43
scenario-44
scenario-45
scenario-46
scenario-47
scenario-48
scenario-49
scenario-50
scenario-51
scenario-52
scenario-53
scenario-54
scenario-55
scenario-56
skills
frameworks
ash-framework
infrastructure
orchestration
elixir-skill-router
personas
phoenix
quality
security
security-essentials
tooling
mix-tasks-generators