Elixir Cheatsheet

Phoenix, Ecto, and Production

Use this Elixir reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Phoenix Route Shape

scope "/", MyAppWeb do
  pipe_through :browser
  get "/", PageController, :home
  live "/dashboard", DashboardLive
end

scope "/api", MyAppWeb do
  pipe_through :api
  resources "/users", UserController, only: [:index, :show, :create]
end

Requests flow through pipelines (plug chains) into controllers, LiveViews, or channels. mix phx.routes lists every route.

Controller Action

def show(conn, %{"id" => id}) do
  user = Accounts.get_user!(id)
  render(conn, :show, user: user)
end

Controllers receive a conn and a string-keyed params map. Keep business logic in contexts (MyApp.Accounts), not controllers.

LiveView

defmodule MyAppWeb.CounterLive do
  use MyAppWeb, :live_view

  def mount(_params, _session, socket) do
    {:ok, assign(socket, count: 0)}
  end

  def handle_event("inc", _params, socket) do
    {:noreply, update(socket, :count, &(&1 + 1))}
  end

  def handle_info({:tick, n}, socket) do      # PubSub/timer messages
    {:noreply, assign(socket, count: n)}
  end

  def render(assigns) do
    ~H"""
    <h1>Count: {@count}</h1>
    <button phx-click="inc">+1</button>
    """
  end
end

A LiveView is a process: mount/3 sets initial assigns, phx-click/phx-submit/phx-change bindings hit handle_event/3, and changed assigns diff-patch the DOM over the websocket. Keep assigns small and durable data in the database; subscribe in mount with Phoenix.PubSub.subscribe/2 and broadcast with Phoenix.PubSub.broadcast/3.

Ecto Schema and Changesets

defmodule MyApp.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :email, :string
    field :admin, :boolean, default: false
    has_many :posts, MyApp.Blog.Post
    belongs_to :team, MyApp.Accounts.Team
    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:email, :admin, :team_id])
    |> validate_required([:email])
    |> validate_format(:email, ~r/@/)
    |> unique_constraint(:email)
    |> foreign_key_constraint(:team_id)
  end
end

Changesets cast and validate external data before it reaches the database; *_constraint functions convert database constraint violations into changeset errors instead of exceptions.

Migrations

mix ecto.gen.migration create_users
mix ecto.migrate
mix ecto.rollback
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string, null: false
      add :team_id, references(:teams, on_delete: :delete_all)
      timestamps()
    end

    create unique_index(:users, [:email])
    create index(:users, [:team_id])
  end
end

Queries and Associations

import Ecto.Query

Repo.get(User, id)                         # nil if missing; get! raises
Repo.get_by(User, email: email)
Repo.all(from u in User, where: u.admin == true, order_by: u.email)
Repo.one(from u in User, select: count(u.id))
Repo.insert(changeset); Repo.update(changeset); Repo.delete(user)

# associations load explicitly — no lazy loading
user = Repo.preload(user, :posts)
Repo.all(from u in User, preload: [posts: :comments])

Repo.all(
  from u in User,
    join: p in assoc(u, :posts),
    where: p.published,
    select: {u.email, count(p.id)},
    group_by: u.email
)

Accessing an unloaded association returns %Ecto.Association.NotLoaded{} — always preload. Parameterized Ecto queries interpolate values with ^ (where: u.email == ^email); never build SQL strings from user input.

Transactions and Ecto.Multi

Repo.transaction(fn ->
  order = Repo.insert!(order_changeset)
  Repo.update!(decrement_stock(item))
  order                                   # {:ok, order}; raise/Repo.rollback aborts
end)

Ecto.Multi.new()
|> Ecto.Multi.insert(:order, order_changeset)
|> Ecto.Multi.update(:stock, fn %{order: o} -> decrement_stock(o) end)
|> Repo.transaction()
|> case do
  {:ok, %{order: order}} -> {:ok, order}
  {:error, step, changeset, _done} -> {:error, step, changeset}
end

Ecto.Multi names each step, threads earlier results into later ones, and tells you exactly which step failed — prefer it over nested Repo.transaction logic.

Releases

MIX_ENV=prod mix assets.deploy
MIX_ENV=prod mix release
_build/prod/rel/my_app/bin/my_app start
_build/prod/rel/my_app/bin/my_app eval "MyApp.Release.migrate()"  # run migrations
_build/prod/rel/my_app/bin/my_app remote                          # IEx into a live node

Releases package the BEAM runtime, compiled code, and configuration hooks for deployment.

Runtime Configuration

# config/runtime.exs
if config_env() == :prod do
  config :my_app, MyApp.Repo, url: System.fetch_env!("DATABASE_URL")

  config :my_app, MyAppWeb.Endpoint,
    secret_key_base: System.fetch_env!("SECRET_KEY_BASE")
end

Use runtime.exs for values only known in the deploy environment — it runs at boot, including inside a release.

Observability

  • Logger.metadata(request_id: id) — attach request/user context to every log line.
  • Phoenix and Ecto emit :telemetry events; attach handlers or use phoenix_live_dashboard in dev.
  • Expose health checks for load balancers; monitor supervisor restart intensity.
  • Track database pool saturation (queue_time in Ecto telemetry) and request latency.
  • Keep crash reports actionable; never log secrets.

Production Rule of Thumb

Pure functions for transformations, contexts for business workflows, changesets for validation, supervised processes for stateful services, telemetry/logging for visibility.