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

  1. Create a new Mix project with a module that calculates fibonacci numbers

  2. Add dependencies for Jason (JSON) and HTTPoison (HTTP client). Fetch data from a public API

  3. Write a custom Mix task that lists all modules in your project

  4. Create an umbrella project with two apps: one for business logic, one for API

  5. 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.