콘텐츠로 이동

Phoenix Cheat Sheet

Overview

Phoenix is a productive web framework for Elixir that implements the server-side MVC pattern. Built on top of the Erlang VM’s concurrency model, Phoenix handles millions of simultaneous connections efficiently, making it ideal for real-time applications. It provides a robust set of tools including LiveView for interactive UIs without JavaScript, Channels for WebSocket communication, and tight Ecto integration for database operations.

Phoenix was created by Chris McCord and has grown into one of the most performant web frameworks available. Its architecture leverages OTP (Open Telecom Platform) patterns for fault tolerance, and the Plug middleware pipeline for composable request handling. Phoenix LiveView, introduced in 2018, revolutionized the framework by enabling rich, real-time user experiences rendered entirely on the server.

Installation

Prerequisites and Setup

# Install Elixir (includes Mix)
brew install elixir     # macOS
sudo apt install elixir # Ubuntu

# Install Hex package manager
mix local.hex

# Install Phoenix project generator
mix archive.install hex phx_new

# Create a new Phoenix project
mix phx.new my_app

# Create without Ecto (no database)
mix phx.new my_app --no-ecto

# Create API-only project
mix phx.new my_api --no-html --no-assets

# Setup and run
cd my_app
mix setup
mix phx.server

Project Structure

PathDescription
lib/my_app/Business logic (contexts, schemas)
lib/my_app_web/Web layer (controllers, views, templates)
lib/my_app_web/router.exRoute definitions
lib/my_app_web/endpoint.exHTTP endpoint configuration
lib/my_app_web/controllers/Controller modules
lib/my_app_web/live/LiveView modules
lib/my_app_web/components/Function components
priv/repo/migrations/Ecto database migrations
priv/static/Static assets
config/Configuration files
test/Test files

Routing

Router Configuration

# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_live_flash
    plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", MyAppWeb do
    pipe_through :browser

    get "/", PageController, :home
    resources "/users", UserController
    live "/dashboard", DashboardLive
  end

  scope "/api", MyAppWeb.API do
    pipe_through :api
    resources "/posts", PostController, except: [:new, :edit]
  end
end

Route Helpers

Route MacroHTTP MethodPathController Action
getGET/users:index
getGET/users/:id:show
getGET/users/new:new
postPOST/users:create
getGET/users/:id/edit:edit
put/patchPUT/PATCH/users/:id:update
deleteDELETE/users/:id:delete

Controllers

defmodule MyAppWeb.UserController do
  use MyAppWeb, :controller

  alias MyApp.Accounts

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, :index, users: users)
  end

  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
      {:ok, user} ->
        conn
        |> put_flash(:info, "User created.")
        |> redirect(to: ~p"/users/#{user}")

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, :new, changeset: changeset)
    end
  end

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

LiveView

Basic LiveView

defmodule MyAppWeb.CounterLive do
  use MyAppWeb, :live_view

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

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

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

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

LiveView Form Handling

defmodule MyAppWeb.UserFormLive do
  use MyAppWeb, :live_view

  alias MyApp.Accounts
  alias MyApp.Accounts.User

  def mount(_params, _session, socket) do
    changeset = Accounts.change_user(%User{})
    {:ok, assign(socket, form: to_form(changeset))}
  end

  def handle_event("validate", %{"user" => params}, socket) do
    changeset =
      %User{}
      |> Accounts.change_user(params)
      |> Map.put(:action, :validate)

    {:noreply, assign(socket, form: to_form(changeset))}
  end

  def handle_event("save", %{"user" => params}, socket) do
    case Accounts.create_user(params) do
      {:ok, _user} ->
        {:noreply,
         socket
         |> put_flash(:info, "User created!")
         |> push_navigate(to: ~p"/users")}

      {:error, changeset} ->
        {:noreply, assign(socket, form: to_form(changeset))}
    end
  end

  def render(assigns) do
    ~H"""
    <.form for={@form} phx-change="validate" phx-submit="save">
      <.input field={@form[:name]} label="Name" />
      <.input field={@form[:email]} type="email" label="Email" />
      <button type="submit">Save</button>
    </.form>
    """
  end
end

Ecto Integration

Schema and Changeset

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

  schema "users" do
    field :name, :string
    field :email, :string
    field :age, :integer
    has_many :posts, MyApp.Blog.Post
    timestamps()
  end

  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :age])
    |> validate_required([:name, :email])
    |> validate_format(:email, ~r/@/)
    |> validate_number(:age, greater_than: 0)
    |> unique_constraint(:email)
  end
end

Context Functions

defmodule MyApp.Accounts do
  import Ecto.Query
  alias MyApp.Repo
  alias MyApp.Accounts.User

  def list_users do
    Repo.all(User)
  end

  def get_user!(id), do: Repo.get!(User, id)

  def create_user(attrs) do
    %User{}
    |> User.changeset(attrs)
    |> Repo.insert()
  end

  def list_users_by_age(min_age) do
    from(u in User, where: u.age >= ^min_age, order_by: [desc: u.age])
    |> Repo.all()
  end
end

Migrations

# Generate migration
mix ecto.gen.migration create_users

# Run migrations
mix ecto.migrate

# Rollback last migration
mix ecto.rollback
defmodule MyApp.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string, null: false
      add :email, :string, null: false
      add :age, :integer
      timestamps()
    end

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

Configuration

Endpoint Configuration

# config/dev.exs
config :my_app, MyAppWeb.Endpoint,
  http: [ip: {127, 0, 0, 1}, port: 4000],
  check_origin: false,
  code_reloader: true,
  debug_errors: true,
  secret_key_base: "dev-secret-key...",
  watchers: [
    esbuild: {Esbuild, :install_and_run, [:my_app, ~w(--sourcemap=inline --watch)]},
    tailwind: {Tailwind, :install_and_run, [:my_app, ~w(--watch)]}
  ]

Production Release

# config/runtime.exs
import Config

if config_env() == :prod do
  config :my_app, MyAppWeb.Endpoint,
    url: [host: System.get_env("PHX_HOST"), port: 443, scheme: "https"],
    http: [port: String.to_integer(System.get_env("PORT") || "4000")],
    secret_key_base: System.fetch_env!("SECRET_KEY_BASE")
end

Advanced Usage

Channels (WebSockets)

# Channel definition
defmodule MyAppWeb.RoomChannel do
  use MyAppWeb, :channel

  def join("room:" <> room_id, _params, socket) do
    {:ok, assign(socket, :room_id, room_id)}
  end

  def handle_in("new_msg", %{"body" => body}, socket) do
    broadcast!(socket, "new_msg", %{body: body})
    {:noreply, socket}
  end
end

PubSub

# Subscribe to topic
Phoenix.PubSub.subscribe(MyApp.PubSub, "notifications:#{user_id}")

# Broadcast to topic
Phoenix.PubSub.broadcast(MyApp.PubSub, "notifications:#{user_id}", {:new_notification, data})

# Handle in LiveView
def handle_info({:new_notification, data}, socket) do
  {:noreply, assign(socket, notification: data)}
end

Mix Tasks

CommandDescription
mix phx.gen.html Accounts User users name:string email:stringGenerate HTML resource
mix phx.gen.json API Post posts title:string body:textGenerate JSON API resource
mix phx.gen.live Blog Post posts title:stringGenerate LiveView resource
mix phx.gen.auth Accounts User usersGenerate authentication system
mix phx.gen.context Blog Post posts title:stringGenerate context only
mix phx.gen.schema Blog.Post posts title:stringGenerate schema only
mix phx.routesList all routes
mix phx.digestCompress and tag static assets

Troubleshooting

ProblemSolution
Port 4000 already in useKill existing process: lsof -i :4000 then kill <pid>
LiveView not connectingCheck WebSocket endpoint in endpoint.ex
Assets not compilingRun mix assets.setup then mix assets.deploy
Migration errorsCheck mix ecto.migrations for pending; run mix ecto.reset in dev
CSRF token errorsEnsure protect_from_forgery plug is in pipeline
(Ecto.NoResultsError)Use Repo.get/2 (returns nil) instead of Repo.get!/2
PubSub messages not receivedVerify subscription topic matches broadcast topic exactly
Deployment crashesCheck runtime.exs env vars are set; run mix release with MIX_ENV=prod