- Understand what Mix is and its role
- Create and structure Mix projects
- Manage dependencies with Hex
- Write and run tests with ExUnit
- Build a complete CLI application
- Understand compilation and releases
Mix is Elixir's build tool that provides:
- Project creation and management
- Dependency management
- Compilation
- Testing
- Task running
- Release management
Think of Mix as Elixir's equivalent to npm, cargo, or gradle.
# Create a new project
mix new my_app
# Output:
# * creating README.md
# * creating .formatter.exs
# * creating .gitignore
# * creating mix.exs
# * creating lib
# * creating lib/my_app.ex
# * creating test
# * creating test/test_helper.exs
# * creating test/my_app_test.exs# Create project with supervision tree
mix new my_app --sup
# This adds an Application modulemy_app/
├── .formatter.exs # Code formatting configuration
├── .gitignore # Git ignore file
├── README.md # Project documentation
├── mix.exs # Project configuration
├── lib/ # Application code
│ └── my_app.ex
└── test/ # Tests
├── test_helper.exs
└── my_app_test.exs
The mix.exs file defines your project:
defmodule MyApp.MixProject do
use Mix.Project
def project do
[
app: :my_app, # Application name
version: "0.1.0", # Version
elixir: "~> 1.15", # Elixir version requirement
start_permanent: Mix.env() == :prod,
deps: deps() # Dependencies
]
end
# Run "mix help compile.app" for more
def application do
[
extra_applications: [:logger] # Extra OTP applications
]
end
# Run "mix help deps" for more
defp deps do
[
# {:dep_from_hexpm, "~> 0.3.0"},
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
]
end
end# Compile project
mix compile
# Run project
mix run
# Start interactive shell with project loaded
iex -S mix
# Format code
mix format
# Clean build artifacts
mix clean
# Show help
mix help
mix help deps # Help for specific task# List all tasks
mix help
# Show dependencies
mix deps
# Show dependency tree
mix deps.tree
# Project version
mix versionHex.pm is Elixir's package manager.
Edit mix.exs:
defp deps do
[
{:poison, "~> 5.0"}, # JSON library
{:httpoison, "~> 2.0"}, # HTTP client
{:timex, "~> 3.7"} # Date/Time library
]
endVersion requirements:
~> 1.0- >= 1.0.0 and < 2.0.0>= 1.0.0- Any version >= 1.0.0== 1.0.0- Exact version~> 1.0.3- >= 1.0.3 and < 1.1.0
# Install/update dependencies
mix deps.get
# Update all dependencies
mix deps.update --all
# Update specific dependency
mix deps.update poison
# Show outdated dependencies
mix hex.outdated
# Clean unused dependencies
mix deps.clean --unused# Search Hex
mix hex.search json
# Get package info
mix hex.info poisonExUnit is Elixir's built-in testing framework.
test/my_app_test.exs:
defmodule MyAppTest do
use ExUnit.Case
doctest MyApp
test "greets the world" do
assert MyApp.hello() == :world
end
end# Run all tests
mix test
# Run specific test file
mix test test/my_app_test.exs
# Run specific test line
mix test test/my_app_test.exs:12
# Run with detailed output
mix test --trace
# Run with coverage
mix test --cover
# Watch mode (requires mix_test_watch)
mix test.watchdefmodule MathTest do
use ExUnit.Case
test "addition" do
assert 1 + 1 == 2
end
test "subtraction" do
result = 5 - 3
assert result == 2
end
test "refute (opposite of assert)" do
refute 1 + 1 == 3
end
test "assert_raise" do
assert_raise ArithmeticError, fn ->
1 / 0
end
end
test "assert_receive (for messages)" do
send(self(), :hello)
assert_receive :hello
end
enddefmodule StringTest do
use ExUnit.Case
describe "String.upcase/1" do
test "converts lowercase to uppercase" do
assert String.upcase("hello") == "HELLO"
end
test "handles empty string" do
assert String.upcase("") == ""
end
end
describe "String.downcase/1" do
test "converts uppercase to lowercase" do
assert String.downcase("HELLO") == "hello"
end
end
enddefmodule DatabaseTest do
use ExUnit.Case
# Runs once before all tests
setup_all do
# Start test database
{:ok, pid} = TestDatabase.start_link()
# Clean up after all tests
on_exit(fn -> TestDatabase.stop(pid) end)
%{db: pid}
end
# Runs before each test
setup do
# Reset database
TestDatabase.clear()
:ok
end
test "insert user", %{db: db} do
# Test uses db from setup_all
assert :ok = TestDatabase.insert(db, :user, %{name: "Alice"})
end
endTest code examples in documentation:
defmodule Calculator do
@doc """
Adds two numbers.
## Examples
iex> Calculator.add(2, 3)
5
iex> Calculator.add(-1, 1)
0
"""
def add(a, b), do: a + b
end
# In test file
defmodule CalculatorTest do
use ExUnit.Case
doctest Calculator # This runs the examples above as tests
endLet's build a complete CLI calculator application.
mix new calculator
cd calculatorlib/calculator.ex:
defmodule Calculator do
@moduledoc """
A simple calculator module.
"""
@doc """
Adds two numbers.
## Examples
iex> Calculator.add(2, 3)
5
"""
def add(a, b) when is_number(a) and is_number(b), do: a + b
@doc """
Subtracts two numbers.
## Examples
iex> Calculator.subtract(5, 3)
2
"""
def subtract(a, b) when is_number(a) and is_number(b), do: a - b
@doc """
Multiplies two numbers.
## Examples
iex> Calculator.multiply(4, 5)
20
"""
def multiply(a, b) when is_number(a) and is_number(b), do: a * b
@doc """
Divides two numbers.
## Examples
iex> Calculator.divide(10, 2)
{:ok, 5.0}
iex> Calculator.divide(10, 0)
{:error, :division_by_zero}
"""
def divide(_a, 0), do: {:error, :division_by_zero}
def divide(a, b) when is_number(a) and is_number(b), do: {:ok, a / b}
@doc """
Evaluates a mathematical expression.
## Examples
iex> Calculator.evaluate("2 + 3")
{:ok, 5}
iex> Calculator.evaluate("10 / 2")
{:ok, 5.0}
"""
def evaluate(expression) do
with {:ok, tokens} <- tokenize(expression),
{:ok, result} <- calculate(tokens) do
{:ok, result}
end
end
defp tokenize(expression) do
parts = String.split(expression, " ", trim: true)
case parts do
[a, op, b] ->
with {num_a, _} <- Float.parse(a),
{num_b, _} <- Float.parse(b) do
{:ok, {num_a, op, num_b}}
else
_ -> {:error, :invalid_numbers}
end
_ ->
{:error, :invalid_expression}
end
end
defp calculate({a, "+", b}), do: {:ok, add(a, b)}
defp calculate({a, "-", b}), do: {:ok, subtract(a, b)}
defp calculate({a, "*", b}), do: {:ok, multiply(a, b)}
defp calculate({a, "/", b}), do: divide(a, b)
defp calculate(_), do: {:error, :unknown_operator}
endlib/calculator/cli.ex:
defmodule Calculator.CLI do
@moduledoc """
CLI interface for the calculator.
"""
def main(args \\ []) do
args
|> parse_args()
|> process()
end
defp parse_args(args) do
case args do
[] -> :interactive
[expression] -> {:expression, expression}
_ -> :help
end
end
defp process(:help) do
IO.puts("""
Calculator CLI
Usage:
calculator "2 + 3" # Evaluate expression
calculator # Interactive mode
Supported operations: +, -, *, /
""")
end
defp process(:interactive) do
IO.puts("Calculator (type 'quit' to exit)")
interactive_loop()
end
defp process({:expression, expr}) do
case Calculator.evaluate(expr) do
{:ok, result} ->
IO.puts("Result: #{result}")
{:error, reason} ->
IO.puts("Error: #{reason}")
end
end
defp interactive_loop do
input = IO.gets("> ") |> String.trim()
case input do
"quit" ->
IO.puts("Goodbye!")
"" ->
interactive_loop()
expression ->
case Calculator.evaluate(expression) do
{:ok, result} ->
IO.puts("= #{result}")
{:error, reason} ->
IO.puts("Error: #{reason}")
end
interactive_loop()
end
end
endtest/calculator_test.exs:
defmodule CalculatorTest do
use ExUnit.Case
doctest Calculator
describe "basic operations" do
test "add/2" do
assert Calculator.add(2, 3) == 5
assert Calculator.add(-1, 1) == 0
end
test "subtract/2" do
assert Calculator.subtract(5, 3) == 2
assert Calculator.subtract(0, 5) == -5
end
test "multiply/2" do
assert Calculator.multiply(4, 5) == 20
assert Calculator.multiply(-2, 3) == -6
end
test "divide/2" do
assert Calculator.divide(10, 2) == {:ok, 5.0}
assert Calculator.divide(7, 2) == {:ok, 3.5}
end
test "divide by zero" do
assert Calculator.divide(10, 0) == {:error, :division_by_zero}
end
end
describe "evaluate/1" do
test "evaluates addition" do
assert Calculator.evaluate("2 + 3") == {:ok, 5}
end
test "evaluates subtraction" do
assert Calculator.evaluate("10 - 7") == {:ok, 3}
end
test "evaluates multiplication" do
assert Calculator.evaluate("4 * 5") == {:ok, 20}
end
test "evaluates division" do
assert Calculator.evaluate("10 / 2") == {:ok, 5.0}
end
test "handles invalid expression" do
assert Calculator.evaluate("invalid") == {:error, :invalid_expression}
end
test "handles division by zero" do
assert Calculator.evaluate("5 / 0") == {:error, :division_by_zero}
end
end
endUpdate mix.exs:
def project do
[
app: :calculator,
version: "0.1.0",
elixir: "~> 1.15",
start_permanent: Mix.env() == :prod,
escript: escript(), # Add this line
deps: deps()
]
end
# Add this function
defp escript do
[main_module: Calculator.CLI]
end# Run tests
mix test
# Build executable
mix escript.build
# Run executable
./calculator "10 + 5"
# Output: Result: 15
# Interactive mode
./calculator
# Calculator (type 'quit' to exit)
# > 5 * 3
# = 15
# > quit
# Goodbye!Elixir supports environment-specific configuration:
config/
├── config.exs # Base configuration
├── dev.exs # Development
├── test.exs # Testing
└── prod.exs # Production
config/config.exs:
import Config
config :my_app,
api_key: "dev_key",
port: 4000
# Import environment specific config
import_config "#{config_env()}.exs"config/prod.exs:
import Config
config :my_app,
api_key: System.get_env("API_KEY"),
port: 8080defmodule MyApp do
def api_key do
Application.get_env(:my_app, :api_key)
end
endMix includes a code formatter:
# Format all files
mix format
# Check if files are formatted
mix format --check-formatted.formatter.exs:
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
line_length: 100
]Add ExDoc dependency:
defp deps do
[
{:ex_doc, "~> 0.30", only: :dev, runtime: false}
]
endGenerate documentation:
mix deps.get
mix docs
# Open docs/index.html in browserdefmodule MyModule do
@moduledoc """
This module does amazing things.
## Examples
iex> MyModule.hello()
"Hello"
"""
@doc """
Says hello.
## Parameters
- name: String - The name to greet
## Examples
iex> MyModule.hello("Alice")
"Hello, Alice"
"""
@spec hello(String.t()) :: String.t()
def hello(name) do
"Hello, #{name}"
end
endYou have learned:
- ✅ What Mix is and its core features
- ✅ How to create and structure Mix projects
- ✅ Managing dependencies with Hex
- ✅ Writing tests with ExUnit
- ✅ Building CLI applications
- ✅ Configuration management
- ✅ Code formatting and documentation
Module 5: Introduction to Phoenix
-
Todo CLI: Build a command-line todo list manager
- Add, list, complete, and delete tasks
- Persist data to a file
- Include full test suite
-
Unit Converter: Create a unit conversion tool
- Temperature (C, F, K)
- Distance (m, km, mi, ft)
- Weight (kg, lb, oz)
- CLI interface
-
CSV Parser: Build a CSV file parser
- Parse CSV to list of maps
- Handle headers
- Export maps to CSV
- Include error handling
-
HTTP Client: Create an HTTP API wrapper
- Use HTTPoison dependency
- Parse JSON responses
- Handle errors gracefully
- Write integration tests
Great work! You're now ready to build Phoenix applications! 🚀