Mix and Projects
Master Mix - Elixir's build tool for creating projects, managing dependencies, running tests, and building releases.
What is Mix?
Mix is Elixir's build tool that provides:
- Project creation and structure
- Dependency management
- Task running (compile, test, run)
- Custom tasks
- Environment management
Check Mix Version
mix --version
# => Mix 1.16.0 (compiled with Erlang/OTP 26)
Creating Projects
New Project
# Create a new project
mix new myapp
# Project structure:
# myapp/
# ├── lib/
# │ └── myapp.ex
# ├── test/
# │ ├── myapp_test.exs
# │ └── test_helper.exs
# ├── mix.exs
# └── README.md
mix.exs - Project Configuration
defmodule Myapp.MixProject do
use Mix.Project
def project do
[
app: :myapp,
version: "0.1.0",
elixir: "~> 1.16",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
def application do
[
extra_applications: [:logger]
]
end
defp deps do
[]
end
end
Supervised Application
# Create with supervision tree
mix new myapp --sup
# Adds application callback:
defmodule Myapp.Application do
use Application
@impl true
def start(_type, _args) do
children = []
opts = [strategy: :one_for_one, name: Myapp.Supervisor]
Supervisor.start_link(children, opts)
end
end
Project Structure
Standard Layout
myapp/
├── _build/ # Compiled artifacts (gitignored)
├── config/ # Configuration files
│ └── config.exs
├── deps/ # Dependencies (gitignored)
├── lib/ # Application source code
│ ├── myapp/
│ │ └── module.ex
│ └── myapp.ex # Main module
├── test/ # Tests
│ ├── myapp_test.exs
│ └── test_helper.exs
├── .formatter.exs # Code formatter config
├── .gitignore
├── mix.exs # Project configuration
└── README.md
lib/ Directory
# lib/myapp.ex - Main module
defmodule Myapp do
@moduledoc """
Documentation for `Myapp`.
"""
def hello do
:world
end
end
# lib/myapp/server.ex - Submodule
defmodule Myapp.Server do
use GenServer
# ...
end
test/ Directory
# test/myapp_test.exs
defmodule MyappTest do
use ExUnit.Case
doctest Myapp
test "greets the world" do
assert Myapp.hello() == :world
end
end
Dependencies
Adding Dependencies
Edit mix.exs:
defp deps do
[
{:jason, "~> 1.4"}, # JSON parser
{:httpoison, "~> 2.0"}, # HTTP client
{:plug_cowboy, "~> 2.6"}, # Web server
{:ecto_sql, "~> 3.10"}, # Database wrapper
{:ex_doc, "~> 0.29", only: :dev, runtime: false}
]
end
Version Requirements
{:package, "~> 1.0"} # >= 1.0.0 and < 2.0.0
{:package, "~> 1.2"} # >= 1.2.0 and < 2.0.0
{:package, "~> 1.2.3"} # >= 1.2.3 and < 1.3.0
{:package, ">= 1.0.0"} # Any version >= 1.0.0
{:package, "== 1.0.0"} # Exactly 1.0.0
{:package, github: "user/repo"} # From GitHub
{:package, path: "../local_dep"} # Local path
Get Dependencies
# Download and compile dependencies
mix deps.get
# Update dependencies
mix deps.update --all
mix deps.update jason
# Show dependency tree
mix deps.tree
# Show outdated dependencies
mix hex.outdated
Environment-Specific Dependencies
defp deps do
[
{:jason, "~> 1.4"},
{:credo, "~> 1.7", only: [:dev, :test], runtime: false},
{:dialyxir, "~> 1.3", only: :dev, runtime: false},
{:ex_doc, "~> 0.29", only: :dev, runtime: false}
]
end
Mix Tasks
Common Tasks
# Compile project
mix compile
# Run project
mix run
# Run with IEx
iex -S mix
# Run tests
mix test
# Format code
mix format
# Create documentation
mix docs
# Clean build artifacts
mix clean
# Show all tasks
mix help
Compilation
# Compile
mix compile
# Force recompilation
mix compile --force
# Compile and show warnings
mix compile --warnings-as-errors
Running Code
# Run without starting application
mix run -e 'IO.puts("Hello")'
# Run script
mix run script.exs
# Start application and keep it running
mix run --no-halt
Testing
# Run all tests
mix test
# Run specific test file
mix test test/myapp_test.exs
# Run specific test line
mix test test/myapp_test.exs:12
# Run with coverage
mix test --cover
# Run failed tests only
mix test --failed
# Watch mode (with mix_test_watch)
mix test.watch
Environments
Mix has three default environments:
:dev (default)
mix compile # Uses :dev environment
:test
mix test # Automatically uses :test
:prod
MIX_ENV=prod mix compile
MIX_ENV=prod mix release
Environment-Specific Config
# config/config.exs
import Config
config :myapp,
setting: "default"
import_config "#{config_env()}.exs"
# config/dev.exs
import Config
config :myapp,
setting: "development"
# config/prod.exs
import Config
config :myapp,
setting: "production"
Runtime Configuration
# config/runtime.exs
import Config
if config_env() == :prod do
config :myapp,
database_url: System.get_env("DATABASE_URL")
end
Creating Custom Tasks
Basic Task
# lib/mix/tasks/hello.ex
defmodule Mix.Tasks.Hello do
use Mix.Task
@shortdoc "Prints Hello"
@moduledoc """
Prints Hello to the console.
## Usage
mix hello
mix hello --name Alice
"""
def run(args) do
{opts, _, _} = OptionParser.parse(args, switches: [name: :string])
name = opts[:name] || "World"
IO.puts("Hello, #{name}!")
end
end
mix hello
# => Hello, World!
mix hello --name Alice
# => Hello, Alice!
Task with Application Started
defmodule Mix.Tasks.DbSeed do
use Mix.Task
@shortdoc "Seeds the database"
def run(_args) do
# Start application
Mix.Task.run("app.start")
# Now you can use your app modules
Myapp.Database.seed()
IO.puts("Database seeded!")
end
end
Task with Dependencies
defmodule Mix.Tasks.Deploy do
use Mix.Task
@shortdoc "Deploys the application"
@requirements ["app.config", "compile"]
def run(_args) do
Mix.shell().info("Deploying...")
# Deployment logic
end
end
Umbrella Projects
Multiple applications in one repository:
Create Umbrella
mix new my_umbrella --umbrella
# Structure:
# my_umbrella/
# ├── apps/
# ├── config/
# └── mix.exs
Add Apps to Umbrella
cd my_umbrella/apps
mix new web --sup
mix new database --sup
mix new core
# Structure:
# my_umbrella/
# ├── apps/
# │ ├── web/
# │ ├── database/
# │ └── core/
# ├── config/
# └── mix.exs
Umbrella Dependencies
# apps/web/mix.exs
defp deps do
[
{:core, in_umbrella: true},
{:database, in_umbrella: true},
{:plug_cowboy, "~> 2.6"}
]
end
Working with Umbrella
# From root, compile all apps
mix compile
# Run tests for all apps
mix test
# Run specific app
cd apps/web
mix test
Releases
Build standalone applications:
Configure Release
# mix.exs
def project do
[
# ...
releases: [
myapp: [
include_executables_for: [:unix],
applications: [runtime_tools: :permanent]
]
]
]
end
Build Release
# Build for production
MIX_ENV=prod mix release
# Output in _build/prod/rel/myapp/
Run Release
# Start release
_build/prod/rel/myapp/bin/myapp start
# Start in daemon mode
_build/prod/rel/myapp/bin/myapp daemon
# Stop release
_build/prod/rel/myapp/bin/myapp stop
# Connect remote console
_build/prod/rel/myapp/bin/myapp remote
Practical Examples
CLI Application
# lib/cli.ex
defmodule Myapp.CLI do
def main(args) do
{opts, args, _} = OptionParser.parse(args,
switches: [help: :boolean, verbose: :boolean],
aliases: [h: :help, v: :verbose]
)
if opts[:help] do
show_help()
else
process(args, opts)
end
end
defp show_help do
IO.puts("""
Usage: myapp [options] <command>
Options:
-h, --help Show this help
-v, --verbose Verbose output
""")
end
defp process(args, opts) do
if opts[:verbose] do
IO.puts("Processing: #{inspect(args)}")
end
# Your logic here
end
end
# mix.exs
def project do
[
# ...
escript: escript()
]
end
defp escript do
[main_module: Myapp.CLI]
end
mix escript.build
./myapp --help
Database Seeding Task
# lib/mix/tasks/db.seed.ex
defmodule Mix.Tasks.Db.Seed do
use Mix.Task
@shortdoc "Seeds the database"
def run(_args) do
Mix.Task.run("app.start")
# Seed data
users = [
%{name: "Alice", email: "alice@example.com"},
%{name: "Bob", email: "bob@example.com"}
]
Enum.each(users, fn user ->
case Myapp.Accounts.create_user(user) do
{:ok, user} -> IO.puts("Created: #{user.name}")
{:error, changeset} -> IO.puts("Failed: #{inspect(changeset)}")
end
end)
IO.puts("Seeding complete!")
end
end
Generator Task
# lib/mix/tasks/gen.module.ex
defmodule Mix.Tasks.Gen.Module do
use Mix.Task
@shortdoc "Generates a new module"
def run([name]) do
module_name = Macro.camelize(name)
file_name = Macro.underscore(name)
content = """
defmodule Myapp.#{module_name} do
@moduledoc \"\"\"
Documentation for #{module_name}.
\"\"\"
def hello do
:world
end
end
"""
path = "lib/myapp/#{file_name}.ex"
File.write!(path, content)
IO.puts("Generated #{path}")
end
end
Custom Formatter Plugin
# .formatter.exs
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:ecto, :phoenix],
locals_without_parens: [
# Custom macros
field: 2,
field: 3
]
]
Configuration
Application Configuration
# config/config.exs
import Config
config :myapp,
port: 4000,
secret_key: "changeme"
config :logger,
level: :info
# Environment specific
import_config "#{config_env()}.exs"
Reading Configuration
# In your code
port = Application.get_env(:myapp, :port)
# or
port = Application.fetch_env!(:myapp, :port)
# With default
port = Application.get_env(:myapp, :port, 4000)
Runtime Configuration
# config/runtime.exs
import Config
if config_env() == :prod do
database_url = System.get_env("DATABASE_URL") ||
raise "DATABASE_URL not set"
config :myapp, Myapp.Repo,
url: database_url,
pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
end
Exercises
Create a new Mix project with a module that calculates fibonacci numbers
Add dependencies for Jason (JSON) and HTTPoison (HTTP client). Fetch data from a public API
Write a custom Mix task that lists all modules in your project
Create an umbrella project with two apps: one for business logic, one for API
Build a CLI tool using escript that reads a file and counts words
# Solutions
# 1. Fibonacci project
mix new fibonacci
cd fibonacci
# lib/fibonacci.ex
defmodule Fibonacci do
def calculate(n) when n <= 1, do: n
def calculate(n), do: calculate(n - 1) + calculate(n - 2)
def sequence(count) do
0..(count - 1)
|> Enum.map(&calculate/1)
end
end
# 2. API fetcher with dependencies
# mix.exs
defp deps do
[
{:jason, "~> 1.4"},
{:httpoison, "~> 2.0"}
]
end
# lib/api_client.ex
defmodule ApiClient do
def fetch_posts do
url = "https://jsonplaceholder.typicode.com/posts"
with {:ok, %{body: body}} <- HTTPoison.get(url),
{:ok, posts} <- Jason.decode(body) do
{:ok, posts}
end
end
end
# 3. Custom task to list modules
# lib/mix/tasks/list_modules.ex
defmodule Mix.Tasks.ListModules do
use Mix.Task
def run(_args) do
Mix.Task.run("compile")
{:ok, modules} = :application.get_key(:myapp, :modules)
IO.puts("Modules in myapp:")
Enum.each(modules, &IO.puts(" - #{&1}"))
end
end
# 4. Umbrella project
mix new my_system --umbrella
cd my_system/apps
mix new business --sup
mix new api --sup
# apps/api/mix.exs
defp deps do
[
{:business, in_umbrella: true},
{:plug_cowboy, "~> 2.6"}
]
end
# 5. Word counter CLI
# lib/word_counter.ex
defmodule WordCounter do
def main(args) do
case args do
[path] ->
count_words(path)
_ ->
IO.puts("Usage: word_counter <file>")
end
end
defp count_words(path) do
case File.read(path) do
{:ok, content} ->
count = content
|> String.split(~r/\s+/)
|> Enum.count()
IO.puts("Word count: #{count}")
{:error, reason} ->
IO.puts("Error: #{reason}")
end
end
end
# mix.exs
def project do
[
# ...
escript: [main_module: WordCounter]
]
end
# Build and run:
# mix escript.build
# ./word_counter myfile.txt
Next Steps
Continue to 10-otp-basics.md to learn about OTP principles, supervisors, applications, and building fault-tolerant systems.