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

  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.