Mix and Projects
Mix is Elixir's build tool. It scaffolds projects, fetches dependencies, runs tests, and builds releases. This chapter walks through the parts you'll touch on day one.
What is Mix?
Mix gives you:
- 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
This adds an 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.