4 min read
On this page

Installation and IEx

Setting up Elixir is straightforward, but the choice of installation method matters more than people admit. If you only ever work on one Elixir project, Homebrew is fine. The moment you have two projects on different versions, or you join a team that pins versions in .tool-versions, you'll wish you'd started with asdf. Save yourself the migration.

Installing With asdf

asdf is a version manager that handles Elixir, Erlang, Node, Ruby, and dozens of other runtimes through plugins. It reads a .tool-versions file in any project to pick the right version automatically, which is how every serious Elixir team works.

# install asdf itself (macOS)
brew install asdf

# add the plugins
asdf plugin add erlang
asdf plugin add elixir

# install a specific version
asdf install erlang 27.1.2
asdf install elixir 1.17.3-otp-27

# set it globally
asdf global erlang 27.1.2
asdf global elixir 1.17.3-otp-27

Always pair an Elixir version with the matching OTP major version. 1.17.3-otp-27 means Elixir 1.17.3 compiled against OTP 27. Mixing OTP versions across machines on a team causes weird crashes that take hours to debug.

In a project, drop a .tool-versions file at the root:

erlang 27.1.2
elixir 1.17.3-otp-27

asdf will pick those up the moment you cd into the directory.

Installing With Homebrew

For quick experiments:

brew install elixir

This pulls in the latest Erlang and Elixir from Homebrew's repos. Fine for tinkering. Don't use it for anything you intend to ship, because you can't pin the version and a brew upgrade will silently change it under you.

Verifying the Install

elixir --version

You should see something like:

Erlang/OTP 27 [erts-15.1.2] [source] [64-bit] [smp:10:10]

Elixir 1.17.3 (compiled with Erlang/OTP 27)

If the Erlang line is missing or shows a different OTP version than you expected, your asdf shims aren't on the PATH. Add . $(brew --prefix asdf)/libexec/asdf.sh to your shell rc file.

The IEx Shell

iex is the Elixir REPL, and it's where you'll spend a surprising amount of time. Unlike Python's REPL, IEx is a full-featured environment: you can attach to running production nodes with it, hot-reload code, inspect process state, and pipe expressions across multiple lines.

iex
Erlang/OTP 27 [erts-15.1.2] [source] [64-bit] [smp:10:10]

Interactive Elixir (1.17.3) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

A few things to know up front:

  • Ctrl+C once shows a menu. Press a to abort, or hit Ctrl+C twice to exit fast.
  • h Module.function shows documentation. Try h Enum.map. This is real, browsable docs from inside the shell.
  • i value inspects any value's type and metadata.
  • recompile() reloads modules from your project after you've edited a file.
iex(1)> h Enum.map

                          def map(enumerable, fun)

  @spec map(t(), (element() -> any())) :: list()

Returns a list where each element is the result of invoking fun on each
corresponding element of enumerable.
...

The built-in help is one of those quality-of-life features you start to miss in other languages.

Mix: The Build Tool

mix is Elixir's project tool — package manager, test runner, task system, and build orchestrator rolled into one. Every Elixir project has a mix.exs file at its root.

mix new my_app

This generates:

my_app/
├── .formatter.exs
├── .gitignore
├── README.md
├── lib/
│   └── my_app.ex
├── mix.exs
└── test/
    ├── my_app_test.exs
    └── test_helper.exs

For an OTP application with a supervision tree (most real projects):

mix new my_app --sup

The --sup flag adds an Application module and starts a supervisor. You'll want this any time you're building a service rather than a library.

Project Structure

Open mix.exs and you'll see two functions: project/0 and application/0.

defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.17",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {MyApp.Application, []}
    ]
  end

  defp deps do
    [
      # {:phoenix, "~> 1.7.0"},
      # {:ecto_sql, "~> 3.10"}
    ]
  end
end

project/0 is metadata for Mix itself. application/0 is what gets baked into the OTP application — it controls which modules start when your app boots.

Add a dependency in deps/0, then run:

mix deps.get

Mix downloads packages from Hex (the package registry) and stores them in deps/. Compilation goes into _build/. Both should be in .gitignore (the generator handles this).

Running Tests

mix test

mix test runs every *_test.exs file under test/. ExUnit, the built-in test framework, is fast and pleasant. Run a single file:

mix test test/my_app_test.exs

Run a single test by line number:

mix test test/my_app_test.exs:14

Run only tests tagged :integration:

mix test --only integration

Most teams set up mix test --cover and mix format --check-formatted in CI. Don't skip the formatter — Elixir's official formatter is opinionated and avoids the bikeshed wars you'd otherwise have on PR review.

The .iex.exs File

This is the trick that makes IEx genuinely useful for daily work. If you put a file called .iex.exs at your project root, IEx evaluates it on startup. You can pre-load aliases, helper functions, sample data, anything.

# .iex.exs
import Ecto.Query
alias MyApp.{Repo, Accounts, Accounts.User}

IO.puts("loaded: Repo, Accounts, User, Ecto.Query")

defmodule H do
  def user(id), do: Repo.get(User, id)
  def users, do: Repo.all(User) |> Enum.take(20)
end

Now when you start iex -S mix, you get:

loaded: Repo, Accounts, User, Ecto.Query
iex(1)> H.user(42)
%MyApp.Accounts.User{id: 42, email: "ada@example.com", ...}

Teams often commit a .iex.exs.example and let each developer copy it to a personal .iex.exs they don't commit. The personal one can have throwaway debugging helpers nobody else needs to see.

Common Pitfalls

Mixing OTP versions across team members. Pin both Erlang and Elixir in .tool-versions. Don't trust "it works on my machine."

Forgetting iex -S mix. Plain iex won't load your project. The -S mix flag tells IEx to start your project's application first, which means your modules and dependencies are available.

Editing files but seeing old code. IEx caches loaded modules. Run recompile() after edits, or restart IEx. Better, use mix test --listen-on-stdin for an auto-running test loop.

Ignoring the formatter. mix format is not optional. CI should fail without it. Editors can run it on save. Code review without formatter wars is a quiet luxury.

Treating .iex.exs as a config file. It's evaluated as Elixir code on startup. Don't put secrets there if you commit it. Don't put expensive setup either, because it runs every time.

Key Takeaways

  • Use asdf for version management from day one. Homebrew is fine for tinkering, painful for teams.
  • mix new --sup for services, mix new for libraries.
  • IEx is a real tool, not a toy. Learn h, i, and recompile().
  • Pin versions in .tool-versions and commit it.
  • A project-level .iex.exs saves hours of re-typing aliases and queries.
  • mix format is non-negotiable. Run it in CI.