# Phoenix Patterns Patterns specific to Phoenix extracted from the framework source code. ## 1. Endpoint as Supervision Tree Root + Plug Pipeline **Source:** `lib/phoenix/endpoint.ex:1-40` (moduledoc) > The endpoint is the boundary where all requests to your web application start. > It is also the interface your application provides to the underlying web servers. > > Overall, an endpoint has three responsibilities: > - to provide a wrapper for starting and stopping the endpoint as part of a supervision tree > - to define an initial plug pipeline for requests to pass through > - to host web specific configuration for your application **Source:** `lib/phoenix/endpoint.ex:408-418` (`__using__` macro) ```elixir defmacro __using__(opts) do quote do @behaviour Phoenix.Endpoint unquote(config(opts)) unquote(pubsub()) unquote(plug()) unquote(server()) end end ``` The endpoint is four things composed together: 1. **Config** — compile-time and runtime configuration 2. **PubSub** — subscribe/broadcast interface 3. **Plug** — request pipeline (via `Plug.Builder`) 4. **Server** — supervision and HTTP server management **Why:** The Endpoint is a supervisor, a plug pipeline, AND a configuration host — all in one module. This unification means one place to configure and start the entire web layer. **Anti-pattern:** Splitting endpoint responsibilities across multiple unrelated modules — Phoenix deliberately consolidates the "boundary" concept. ### When to Use **Triggers:** - You need a single entry point for all HTTP requests - You want unified config, PubSub, and server supervision in one module - You are starting a new Phoenix application or adding a separate web interface (e.g., admin endpoint) **Example — before:** ```elixir # Scattered config, manual PubSub setup, separate server management defmodule MyApp.Application do def start(_type, _args) do children = [ {Phoenix.PubSub, name: MyApp.PubSub}, MyApp.WebServer, MyApp.ConfigServer ] Supervisor.start_link(children, strategy: :one_for_one) end end ``` **Example — after:** ```elixir # Unified in endpoint — config, PubSub, plug pipeline, server all in one defmodule MyAppWeb.Endpoint do use Phoenix.Endpoint, otp_app: :my_app plug Plug.RequestId plug Plug.Telemetry, event_prefix: [:phoenix, :endpoint] plug MyAppWeb.Router end ``` ### When NOT to Use **Don't use this when:** You have non-HTTP services (background workers, scheduled tasks, message consumers) that don't need a web boundary. **Over-application example:** ```elixir # Bad: Using an Endpoint for a pure background job system defmodule MyApp.WorkerEndpoint do use Phoenix.Endpoint, otp_app: :my_app # No plugs, no routes — just using it as a supervisor end ``` **Better alternative:** ```elixir # Use a plain Supervisor for non-HTTP concerns defmodule MyApp.Workers.Supervisor do use Supervisor def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) end def init(_opts) do children = [MyApp.Workers.EmailSender, MyApp.Workers.DataImporter] Supervisor.init(children, strategy: :one_for_one) end end ``` **Why:** The Endpoint carries HTTP-specific baggage (Plug pipeline, request telemetry, cowboy/bandit server). Using it for non-HTTP work adds unnecessary complexity and confuses the architecture. --- ## 2. Router: Compile-Time Route Optimization **Source:** `lib/phoenix/router.ex:106-128` (Why the macros? info block) > We use macros for two purposes: > > * They define the routing engine, used on every request, to choose which > controller to dispatch the request to. Thanks to macros, Phoenix compiles > all of your routes to a single case-statement with pattern matching rules, > which is heavily optimized by the Erlang VM > > * For each route you define, we also define metadata to implement `Phoenix.VerifiedRoutes`. **Source:** `lib/phoenix/router.ex:299` (route accumulation) ```elixir Module.register_attribute(__MODULE__, :phoenix_routes, accumulate: true) ``` **Why:** Routes are defined with macros that accumulate route data at compile time. At `@before_compile`, all routes are compiled into a single pattern-match dispatch function. This is O(1) routing, not O(n) list scanning. **Anti-pattern:** Runtime route tables (like maps or lists that are scanned per-request) — compile-time pattern matching is orders of magnitude faster. ### When to Use **Triggers:** - You are defining URL-to-controller mappings for your web app - You want the fastest possible request dispatch - You need verified routes (compile-time checked paths) **Example — before:** ```elixir # Runtime route lookup — slow, no compile-time verification defmodule MyRouter do @routes %{ {"GET", "/users"} => {UserController, :index}, {"GET", "/users/:id"} => {UserController, :show} } def dispatch(conn) do case Map.get(@routes, {conn.method, conn.path_info}) do {controller, action} -> apply(controller, action, [conn, conn.params]) nil -> send_resp(conn, 404, "Not Found") end end end ``` **Example — after:** ```elixir # Compile-time route optimization — O(1) dispatch via pattern matching defmodule MyAppWeb.Router do use Phoenix.Router scope "/", MyAppWeb do pipe_through :browser get "/users", UserController, :index get "/users/:id", UserController, :show end end ``` ### When NOT to Use **Don't use this when:** Routes need to be dynamic at runtime (e.g., user-configured URL mappings, plugin systems where routes are registered after compilation). **Over-application example:** ```elixir # Bad: Trying to define routes dynamically via database defmodule MyAppWeb.Router do use Phoenix.Router # This won't work — routes are compiled, not runtime-configurable for route <- Repo.all(CustomRoute) do get route.path, PageController, :dynamic end end ``` **Better alternative:** ```elixir # Use a catch-all route that dispatches based on runtime data defmodule MyAppWeb.Router do use Phoenix.Router scope "/", MyAppWeb do get "/pages/*path", DynamicPageController, :show end end defmodule MyAppWeb.DynamicPageController do use MyAppWeb, :controller def show(conn, %{"path" => path}) do case Pages.find_by_path(Enum.join(path, "/")) do {:ok, page} -> render(conn, :show, page: page) :error -> send_resp(conn, 404, "Not found") end end end ``` **Why:** Phoenix routes are compiled into pattern matches — they cannot change at runtime. For dynamic routing, use a catch-all route that delegates to runtime logic. --- ## 3. Pipeline and `pipe_through` for Request Processing **Source:** `lib/phoenix/router.ex:243-270` (Pipelines and plugs section) ```elixir pipeline :browser do plug :fetch_session plug :accepts, ["html"] end scope "/" do pipe_through :browser # routes end ``` **Why:** Pipelines are named, composable groups of plugs. Routes declare which pipelines they pass through. This separates concerns: - Pipeline definition (what transformations exist) - Route definition (which routes use which pipelines) **Anti-pattern:** Putting plug logic directly in controllers or duplicating plug chains across routes. ### When to Use **Triggers:** - Multiple routes need the same set of plugs (auth, session, content type) - You have different request processing needs (browser vs API vs admin) - You want to DRY up repeated plug declarations **Example — before:** ```elixir # Duplicated plug logic in every controller defmodule MyAppWeb.UserController do use MyAppWeb, :controller plug :fetch_session plug :require_auth plug :put_layout, html: {MyAppWeb.Layouts, :app} def index(conn, _params), do: # ... end defmodule MyAppWeb.PostController do use MyAppWeb, :controller plug :fetch_session plug :require_auth plug :put_layout, html: {MyAppWeb.Layouts, :app} def index(conn, _params), do: # ... end ``` **Example — after:** ```elixir # Pipelines in router — declared once, reused everywhere defmodule MyAppWeb.Router do use Phoenix.Router pipeline :browser do plug :fetch_session plug :put_layout, html: {MyAppWeb.Layouts, :app} end pipeline :authenticated do plug :require_auth end scope "/", MyAppWeb do pipe_through [:browser, :authenticated] resources "/users", UserController resources "/posts", PostController end end ``` ### When NOT to Use **Don't use this when:** A plug is specific to a single controller action or needs per-action conditional logic. **Over-application example:** ```elixir # Bad: Creating a pipeline for something only one action needs pipeline :with_special_header do plug :add_special_header end scope "/", MyAppWeb do pipe_through [:browser, :with_special_header] get "/special", SpecialController, :show # only route using this end ``` **Better alternative:** ```elixir # Controller-level plug with guard for action-specific behavior defmodule MyAppWeb.SpecialController do use MyAppWeb, :controller plug :add_special_header when action == :show def show(conn, _params), do: # ... defp add_special_header(conn, _opts) do put_resp_header(conn, "x-special", "true") end end ``` **Why:** Pipelines are for shared cross-cutting concerns. Single-action or single-controller logic belongs in the controller itself, keeping the router clean and pipelines meaningful. --- ## 4. Controller as Thin Dispatch Layer **Source:** `lib/phoenix/controller.ex:28-45` (moduledoc examples) ```elixir defmodule MyAppWeb.UserController do use MyAppWeb, :controller def show(conn, %{"id" => id}) do user = Repo.get(User, id) render(conn, :show, user: user) end end ``` Controllers: - Pattern match on params (destructure what you need) - Call domain logic (the Repo/context layer) - Render the result **Source:** `lib/phoenix/controller.ex:1-5` (imports) ```elixir defmodule Phoenix.Controller do import Plug.Conn alias Plug.Conn.AlreadySentError require Logger ``` **Why:** Controllers import `Plug.Conn` for connection manipulation. They're pluggable themselves — a controller IS a plug. The action is just the last step in the plug pipeline. **Anti-pattern:** Fat controllers with business logic — controllers should delegate to context modules. ### When to Use **Triggers:** - You need to handle an HTTP request and return a response - The work is: receive params → call domain logic → render result - You want a clean boundary between web concerns and business logic **Example — before:** ```elixir # Fat controller with business logic mixed in defmodule MyAppWeb.OrderController do use MyAppWeb, :controller def create(conn, %{"order" => order_params}) do user = conn.assigns.current_user items = Repo.all(from i in Item, where: i.id in ^order_params["item_ids"]) total = Enum.reduce(items, 0, &(&1.price + &2)) discount = if user.membership == :premium, do: total * 0.1, else: 0 changeset = Order.changeset(%Order{}, %{ user_id: user.id, total: total - discount, items: items }) case Repo.insert(changeset) do {:ok, order} -> Mailer.deliver(OrderEmail.confirmation(order)) conn |> put_flash(:info, "Order placed!") |> redirect(to: ~p"/orders/#{order}") {:error, changeset} -> render(conn, :new, changeset: changeset) end end end ``` **Example — after:** ```elixir # Thin controller — delegates to context defmodule MyAppWeb.OrderController do use MyAppWeb, :controller def create(conn, %{"order" => order_params}) do case Orders.place_order(conn.assigns.current_user, order_params) do {:ok, order} -> conn |> put_flash(:info, "Order placed!") |> redirect(to: ~p"/orders/#{order}") {:error, changeset} -> render(conn, :new, changeset: changeset) end end end ``` ### When NOT to Use **Don't use this when:** You're building a LiveView-only application with no traditional request/response cycle, or when the "controller" would just be a pass-through with no meaningful web-layer work. **Over-application example:** ```elixir # Bad: Controller that just proxies to LiveView defmodule MyAppWeb.DashboardController do use MyAppWeb, :controller def index(conn, _params) do # All this does is redirect to LiveView — unnecessary layer redirect(conn, to: ~p"/dashboard/live") end end ``` **Better alternative:** ```elixir # Route directly to the LiveView scope "/", MyAppWeb do pipe_through :browser live "/dashboard", DashboardLive end ``` **Why:** Controllers exist to mediate between HTTP and your domain. If there's no mediation needed (no params to validate, no flash messages, no redirects based on outcome), routing directly to a LiveView or using `plug` functions is simpler. --- ## 5. Channel as GenServer with Topic-Based Routing **Source:** `lib/phoenix/channel.ex:1-25` (topic pattern) ```elixir channel "room:*", MyAppWeb.RoomChannel ``` Then in the channel: ```elixir def join("room:lobby", _payload, socket) do {:ok, socket} end def join("room:" <> room_id, _payload, socket) do {:ok, socket} end ``` **Source:** `lib/phoenix/channel.ex:474-478` (channels are GenServers) ```elixir def start_link(triplet) do GenServer.start_link(Phoenix.Channel.Server, triplet, hibernate_after: @phoenix_hibernate_after ) end ``` **Why:** Each channel join creates a process. Pattern matching on the topic string provides natural routing. The GenServer backing means channels get supervision, hibernation, and all OTP semantics. **Anti-pattern:** Managing channel state in shared ETS or external state — each channel IS its own process with its own state. ### When to Use **Triggers:** - You need real-time bidirectional communication (chat, notifications, live updates) - Each connected client needs its own isolated state - You want pub/sub semantics with topic-based routing - You need presence tracking (who's online) **Example — before:** ```elixir # Polling API endpoint — client hits this every 2 seconds defmodule MyAppWeb.NotificationController do use MyAppWeb, :controller def index(conn, _params) do user = conn.assigns.current_user notifications = Notifications.unread_for(user) json(conn, %{notifications: notifications}) end end ``` **Example — after:** ```elixir # Real-time channel — server pushes when notifications arrive defmodule MyAppWeb.NotificationChannel do use Phoenix.Channel def join("notifications:" <> user_id, _payload, socket) do if socket.assigns.user_id == user_id do {:ok, socket} else {:error, %{reason: "unauthorized"}} end end def handle_info({:new_notification, notification}, socket) do push(socket, "new_notification", notification) {:noreply, socket} end end ``` ### When NOT to Use **Don't use this when:** Communication is unidirectional (server → client only) with no per-client state, or when you need to fan out identical data to many thousands of clients without per-connection processes. **Over-application example:** ```elixir # Bad: Channel for read-only server-sent data with no client interaction defmodule MyAppWeb.StockTickerChannel do use Phoenix.Channel def join("ticker:prices", _payload, socket) do # Every client gets identical data, no per-client state send(self(), :send_prices) {:ok, socket} end def handle_info(:send_prices, socket) do push(socket, "prices", get_all_prices()) Process.send_after(self(), :send_prices, 1000) {:noreply, socket} end end ``` **Better alternative:** ```elixir # Use PubSub broadcast — one process publishes, all subscribers receive defmodule MyApp.StockTicker do use GenServer def handle_info(:tick, state) do prices = fetch_prices() Phoenix.PubSub.broadcast(MyApp.PubSub, "ticker:prices", {:prices, prices}) Process.send_after(self(), :tick, 1000) {:noreply, state} end end # Minimal channel just subscribes — no per-client timer/logic defmodule MyAppWeb.StockTickerChannel do use Phoenix.Channel def join("ticker:prices", _payload, socket) do Phoenix.PubSub.subscribe(MyApp.PubSub, "ticker:prices") {:ok, socket} end def handle_info({:prices, prices}, socket) do push(socket, "prices", prices) {:noreply, socket} end end ``` **Why:** Each channel process consumes memory and CPU. If all clients receive identical data and have no individual state, use PubSub broadcast from a single GenServer rather than duplicating fetch logic in thousands of channel processes. --- ## 6. PubSub Integration via Endpoint **Source:** `lib/phoenix/endpoint.ex:437-475` (pubsub macro) ```elixir def subscribe(topic, opts \\ []) when is_binary(topic) do Phoenix.PubSub.subscribe(pubsub_server!(), topic, opts) end def broadcast(topic, event, msg) do Phoenix.Channel.Server.broadcast(pubsub_server!(), topic, event, msg) end defp pubsub_server! do config(:pubsub_server) || raise ArgumentError, "no :pubsub_server configured for #{inspect(__MODULE__)}" end ``` **Why:** PubSub is wired through the endpoint — `MyAppWeb.Endpoint.broadcast!("topic", "event", payload)`. The endpoint knows its pubsub server from config; channels broadcast through it transparently. **Anti-pattern:** Passing PubSub server names around manually — the endpoint already knows and exposes the interface. ### When to Use **Triggers:** - You need to broadcast events from contexts/services to connected clients - You want a single consistent PubSub interface across your application - You are sending messages from outside a channel (e.g., from a context module after a DB write) **Example — before:** ```elixir # Manually passing PubSub server name everywhere defmodule MyApp.Posts do def create_post(attrs) do case Repo.insert(Post.changeset(%Post{}, attrs)) do {:ok, post} -> # Have to know the PubSub name and wire it through Phoenix.PubSub.broadcast(MyApp.PubSub, "posts:feed", {:new_post, post}) {:ok, post} error -> error end end end ``` **Example — after:** ```elixir # Use the Endpoint's PubSub interface — already configured defmodule MyApp.Posts do def create_post(attrs) do case Repo.insert(Post.changeset(%Post{}, attrs)) do {:ok, post} -> MyAppWeb.Endpoint.broadcast!("posts:feed", "new_post", %{id: post.id, title: post.title}) {:ok, post} error -> error end end end ``` ### When NOT to Use **Don't use this when:** You need PubSub between non-web services that have no Phoenix Endpoint (e.g., distributed GenServers communicating across nodes without a web layer). **Over-application example:** ```elixir # Bad: Referencing the web endpoint from a pure domain library defmodule MyApp.Analytics.Aggregator do # This pure data processing module shouldn't know about web endpoints def aggregate(data) do result = compute(data) MyAppWeb.Endpoint.broadcast!("analytics:results", "update", result) result end end ``` **Better alternative:** ```elixir # Use Phoenix.PubSub directly when you're not in the web layer defmodule MyApp.Analytics.Aggregator do def aggregate(data) do result = compute(data) Phoenix.PubSub.broadcast(MyApp.PubSub, "analytics:results", {:update, result}) result end end ``` **Why:** `Endpoint.broadcast!/3` is a convenience wrapper. In non-web modules, depending on the endpoint creates a circular dependency (domain → web). Use `Phoenix.PubSub` directly to keep the dependency arrow clean: web depends on domain, not the reverse. --- ## 7. Socket as Authentication Boundary **Source:** `lib/phoenix/socket.ex:1-30` (moduledoc: connect callback pattern) ```elixir defmodule MyAppWeb.UserSocket do use Phoenix.Socket channel "room:*", MyAppWeb.RoomChannel def connect(params, socket, _connect_info) do {:ok, assign(socket, :user_id, params["user_id"])} end def id(socket), do: "users_socket:#{socket.assigns.user_id}" end ``` **Why:** Authentication happens ONCE at socket connection. All channels on that socket inherit the authenticated identity. `id/1` enables targeted disconnection — `Endpoint.broadcast("users_socket:123", "disconnect", %{})`. **Anti-pattern:** Authenticating in every `join/3` callback instead of at the socket level. ### When to Use **Triggers:** - You need authenticated real-time connections - Multiple channels share the same user identity - You want the ability to force-disconnect specific users - Authentication happens via token/session that should be validated once **Example — before:** ```elixir # Bad: Re-authenticating in every channel join defmodule MyAppWeb.ChatChannel do use Phoenix.Channel def join("chat:" <> room_id, %{"token" => token}, socket) do case verify_token(token) do {:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)} :error -> {:error, %{reason: "unauthorized"}} end end end defmodule MyAppWeb.NotifChannel do use Phoenix.Channel def join("notif:" <> user_id, %{"token" => token}, socket) do # Same auth logic duplicated! case verify_token(token) do {:ok, uid} -> {:ok, assign(socket, :user_id, uid)} :error -> {:error, %{reason: "unauthorized"}} end end end ``` **Example — after:** ```elixir # Auth once at socket level — all channels inherit identity defmodule MyAppWeb.UserSocket do use Phoenix.Socket channel "chat:*", MyAppWeb.ChatChannel channel "notif:*", MyAppWeb.NotifChannel def connect(%{"token" => token}, socket, _connect_info) do case Phoenix.Token.verify(MyAppWeb.Endpoint, "user", token, max_age: 86400) do {:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)} {:error, _} -> :error end end def id(socket), do: "users_socket:#{socket.assigns.user_id}" end # Channels just use socket.assigns — already authenticated defmodule MyAppWeb.ChatChannel do use Phoenix.Channel def join("chat:" <> room_id, _payload, socket) do if authorized?(socket.assigns.user_id, room_id) do {:ok, socket} else {:error, %{reason: "unauthorized"}} end end end ``` ### When NOT to Use **Don't use this when:** You have unauthenticated/public channels where no identity is needed, or each channel requires different credentials (multi-tenant with separate auth per service). **Over-application example:** ```elixir # Bad: Forcing authentication on a public broadcast socket defmodule MyAppWeb.PublicSocket do use Phoenix.Socket channel "announcements:*", MyAppWeb.AnnouncementChannel def connect(%{"token" => token}, socket, _connect_info) do # Public announcements don't need auth — this blocks anonymous viewers case verify_token(token) do {:ok, user_id} -> {:ok, assign(socket, :user_id, user_id)} {:error, _} -> :error end end end ``` **Better alternative:** ```elixir # Accept all connections for public channels defmodule MyAppWeb.PublicSocket do use Phoenix.Socket channel "announcements:*", MyAppWeb.AnnouncementChannel def connect(_params, socket, _connect_info) do {:ok, socket} end def id(_socket), do: nil # No targeted disconnect needed end ``` **Why:** Not every socket needs authentication. Public broadcast channels (announcements, live scores, status pages) should accept connections freely. Forcing auth adds friction and excludes legitimate anonymous users. --- ## 8. Plug Pattern: `init/1` + `call/2` **Source:** `lib/phoenix/router/route.ex:47-57` ```elixir @doc "Used as a plug on forwarding" def init(opts), do: opts @doc "Used as a plug on forwarding" def call(%{path_info: path, script_name: script} = conn, {fwd_segments, plug, opts}) do new_path = path -- fwd_segments {base, ^new_path} = Enum.split(path, length(path) - length(new_path)) conn = %{conn | path_info: new_path, script_name: script ++ base} conn = plug.call(conn, plug.init(opts)) %{conn | path_info: path, script_name: script} end ``` **Why:** The Plug specification splits work into: - `init/1` — compile-time setup (called once, result cached) - `call/2` — runtime execution (called per-request, must be fast) This is Phoenix's fundamental composition pattern. Everything is a plug. **Anti-pattern:** Doing expensive setup work in `call/2` instead of `init/1` — it runs on every request. ### When to Use **Triggers:** - You need middleware (request/response transformation) - You want composable, reusable request processing steps - You have expensive setup (regex compilation, config parsing) that should run once **Example — before:** ```elixir # Bad: Expensive work on every request defmodule MyApp.RateLimiter do def call(conn, _opts) do # Compiling regex on EVERY request pattern = Regex.compile!("^/api/") config = Application.get_env(:my_app, :rate_limit) # disk read each time bucket_size = config[:bucket_size] if Regex.match?(pattern, conn.request_path) do check_rate(conn, bucket_size) else conn end end end ``` **Example — after:** ```elixir # Correct: Expensive work in init/1, fast work in call/2 defmodule MyApp.RateLimiter do @behaviour Plug def init(opts) do %{ pattern: Regex.compile!(Keyword.get(opts, :path_pattern, "^/api/")), bucket_size: Keyword.get(opts, :bucket_size, 100) } end def call(conn, %{pattern: pattern, bucket_size: bucket_size}) do if Regex.match?(pattern, conn.request_path) do check_rate(conn, bucket_size) else conn end end end ``` ### When NOT to Use **Don't use this when:** The logic is inherently per-request and cannot be split (e.g., the "init" data depends on the request itself), or when you are writing a simple helper function, not middleware. **Over-application example:** ```elixir # Bad: Forcing Plug pattern on a simple utility function defmodule MyApp.Helpers.FormatDate do @behaviour Plug def init(opts), do: opts def call(conn, _opts) do # This doesn't transform the connection — it's not middleware assign(conn, :formatted_date, Calendar.strftime(Date.utc_today(), "%B %d, %Y")) end end ``` **Better alternative:** ```elixir # Just a function in your view helpers defmodule MyAppWeb.Helpers do def formatted_date do Calendar.strftime(Date.utc_today(), "%B %d, %Y") end end ``` **Why:** Plugs are for transforming the connection as it flows through a pipeline. If something doesn't need request/response context or pipeline composition, a plain function is simpler and more testable. --- ## 9. Telemetry Integration in Router Dispatch **Source:** `lib/phoenix/router.ex:400-438` (telemetry instrumentation) ```elixir def __call__(conn, metadata, prepare, pipeline, {plug, opts}) do conn = prepare.(conn, metadata) start = System.monotonic_time() measurements = %{system_time: System.system_time()} metadata = %{metadata | conn: conn} :telemetry.execute([:phoenix, :router_dispatch, :start], measurements, metadata) case pipeline.(conn) do %Plug.Conn{halted: true} = halted_conn -> measurements = %{duration: System.monotonic_time() - start} metadata = %{metadata | conn: halted_conn} :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata) halted_conn %Plug.Conn{} = piped_conn -> try do plug.call(piped_conn, plug.init(opts)) else conn -> measurements = %{duration: System.monotonic_time() - start} metadata = %{metadata | conn: conn} :telemetry.execute([:phoenix, :router_dispatch, :stop], measurements, metadata) conn rescue e in Plug.Conn.WrapperError -> measurements = %{duration: System.monotonic_time() - start} :telemetry.execute([:phoenix, :router_dispatch, :exception], measurements, metadata) Plug.Conn.WrapperError.reraise(e) end end end ``` **Source:** `lib/phoenix/logger.ex:7-50` (telemetry event catalog) Phoenix emits these telemetry events: - `[:phoenix, :endpoint, :init]` — endpoint supervision tree started - `[:phoenix, :endpoint, :start]` — request begins (via `Plug.Telemetry`) - `[:phoenix, :endpoint, :stop]` — response sent - `[:phoenix, :router_dispatch, :start]` — route dispatch begins - `[:phoenix, :router_dispatch, :stop]` — route dispatch succeeds - `[:phoenix, :router_dispatch, :exception]` — route dispatch raises - `[:phoenix, :socket_connected]` — socket connection established **Why:** Telemetry is baked into every request path. The router wraps ALL dispatches in start/stop/exception telemetry, enabling monitoring, tracing (OpenTelemetry), and logging without modifying application code. **Anti-pattern:** Manual timing/logging in controllers — telemetry provides this automatically at the infrastructure level. ### When to Use **Triggers:** - You need request duration metrics, error rate monitoring, or distributed tracing - You want observability without modifying business logic - You are integrating with monitoring tools (Prometheus, Datadog, OpenTelemetry) **Example — before:** ```elixir # Bad: Manual timing in every controller action defmodule MyAppWeb.UserController do use MyAppWeb, :controller require Logger def index(conn, _params) do start = System.monotonic_time() users = Accounts.list_users() duration = System.monotonic_time() - start Logger.info("UserController.index took #{System.convert_time_unit(duration, :native, :millisecond)}ms") render(conn, :index, users: users) end end ``` **Example — after:** ```elixir # Correct: Attach telemetry handlers once, get metrics for all routes defmodule MyApp.Telemetry do def setup do :telemetry.attach_many("my-app-telemetry", [ [:phoenix, :router_dispatch, :stop], [:phoenix, :router_dispatch, :exception] ], &handle_event/4, nil) end def handle_event([:phoenix, :router_dispatch, :stop], %{duration: duration}, metadata, _config) do route = "#{metadata.route}" :telemetry.execute([:my_app, :request, :duration], %{value: duration}, %{route: route}) end end ``` ### When NOT to Use **Don't use this when:** You need fine-grained timing of specific business logic steps within a single action — telemetry at the router level only measures total dispatch time. **Over-application example:** ```elixir # Bad: Trying to use router telemetry to measure individual DB queries # Router telemetry measures the whole dispatch — not sub-operations :telemetry.attach("db-timing", [:phoenix, :router_dispatch, :stop], fn _, %{duration: d}, _, _ -> # This is total request time, NOT db time Metrics.record_db_time(d) end, nil) ``` **Better alternative:** ```elixir # Use Ecto telemetry for DB-specific metrics :telemetry.attach("ecto-timing", [:my_app, :repo, :query], fn _, measurements, metadata, _ -> Metrics.record_db_time(measurements.total_time, %{ source: metadata.source, query: metadata.query }) end, nil) ``` **Why:** Phoenix router telemetry operates at the request boundary. For sub-operation metrics (DB queries, HTTP calls, cache lookups), attach to the appropriate library's telemetry events instead. --- ## 10. ConnTest Pattern: Endpoint-Based Integration Testing **Source:** `lib/phoenix/test/conn_test.ex:1-30` (moduledoc) ```elixir @endpoint MyAppWeb.Endpoint test "says welcome on the home page" do conn = get(build_conn(), "/") assert conn.resp_body =~ "Welcome!" end test "logs in" do conn = post(build_conn(), "/login", [username: "john", password: "doe"]) assert conn.resp_body =~ "Logged in!" end ``` **Why:** `Phoenix.ConnTest` tests against the full endpoint stack (plugs, router, controller) without starting an HTTP server. `build_conn()` creates a test connection, HTTP verb functions dispatch through the endpoint. This gives integration-level confidence with unit-test speed. **Anti-pattern:** Testing controllers in isolation without the plug pipeline — you miss middleware bugs (auth, CSRF, sessions). ### When to Use **Triggers:** - You want to test a request through the full plug pipeline - You need to verify auth, CSRF, session, and content negotiation work together - You want fast integration tests without HTTP overhead **Example — before:** ```elixir # Bad: Testing controller function directly — bypasses all middleware test "shows user" do conn = %Plug.Conn{params: %{"id" => "1"}, assigns: %{current_user: user}} result = MyAppWeb.UserController.show(conn, %{"id" => "1"}) # This misses: auth plug, CSRF check, session, layout rendering assert result.status == 200 end ``` **Example — after:** ```elixir # Correct: Test through the endpoint — exercises the full stack defmodule MyAppWeb.UserControllerTest do use MyAppWeb.ConnCase test "requires authentication", %{conn: conn} do conn = get(conn, ~p"/users/1") assert redirected_to(conn) == ~p"/login" end test "shows user when authenticated", %{conn: conn} do user = insert(:user) conn = conn |> log_in_user(user) |> get(~p"/users/#{user}") assert html_response(conn, 200) =~ user.name end end ``` ### When NOT to Use **Don't use this when:** You are testing pure domain logic (context modules, schemas, business rules) that has no HTTP concerns. **Over-application example:** ```elixir # Bad: Using ConnTest to test business logic defmodule MyApp.OrdersTest do use MyAppWeb.ConnCase # unnecessary — no HTTP needed test "calculates discount" do # Making an HTTP request just to test a calculation conn = post(build_conn(), "/api/orders/calculate", %{items: [%{price: 100}], coupon: "SAVE10"}) assert json_response(conn, 200)["total"] == 90 end end ``` **Better alternative:** ```elixir # Test the context directly — faster, clearer, no HTTP noise defmodule MyApp.OrdersTest do use MyApp.DataCase test "calculates discount" do assert Orders.calculate_total([%{price: 100}], coupon: "SAVE10") == 90 end end ``` **Why:** ConnTest is for testing HTTP behavior (status codes, redirects, headers, rendered HTML). Domain logic should have its own tests that run faster and test more directly. --- ## 11. ChannelTest Pattern: Process-Based Channel Testing **Source:** `lib/phoenix/test/channel_test.ex:1-30` (moduledoc) ```elixir {:ok, _, socket} = socket(UserSocket, "user:id", %{some_assigns: 1}) |> subscribe_and_join(RoomChannel, "room:lobby", %{"id" => 3}) # Or using connect/3 to call your UserSocket.connect callback: {:ok, socket} = connect(UserSocket, %{"some" => "params"}, %{}) {:ok, _, socket} = subscribe_and_join(socket, "room:lobby", %{"id" => 3}) ``` **Why:** Channel tests communicate via messages (not HTTP). `subscribe_and_join/4` connects a test process to the channel, and you can assert on broadcasts (`assert_broadcast`), pushes (`assert_push`), and replies (`assert_reply`). The test process subscribes to the same PubSub topic, so it sees everything the channel broadcasts. **Anti-pattern:** Testing channels by connecting real WebSocket clients — too slow, too brittle, tests the transport layer unnecessarily. ### When to Use **Triggers:** - You need to test channel join authorization logic - You want to verify broadcasts, pushes, and replies - You need to test the full channel lifecycle (join → handle_in → leave) **Example — before:** ```elixir # Bad: Testing channels via real WebSocket connections test "user can join and receive messages" do {:ok, socket} = Phoenix.ChannelTest.connect(MyAppWeb.UserSocket, %{}) # Spinning up a real WebSocket client to test logic {:ok, client} = WebSocketClient.connect("ws://localhost:4001/socket/websocket") WebSocketClient.send(client, %{topic: "room:lobby", event: "phx_join", payload: %{}}) assert_receive %{event: "phx_reply", payload: %{"status" => "ok"}} end ``` **Example — after:** ```elixir # Correct: Process-based testing — fast, deterministic defmodule MyAppWeb.RoomChannelTest do use MyAppWeb.ChannelCase test "joins successfully and broadcasts presence" do {:ok, _, socket} = socket(MyAppWeb.UserSocket, "user:1", %{user_id: 1}) |> subscribe_and_join(MyAppWeb.RoomChannel, "room:lobby") assert_broadcast "presence_state", %{} end test "handles new message and broadcasts to room" do {:ok, _, socket} = socket(MyAppWeb.UserSocket, "user:1", %{user_id: 1}) |> subscribe_and_join(MyAppWeb.RoomChannel, "room:lobby") push(socket, "new_msg", %{"body" => "hello"}) assert_broadcast "new_msg", %{"body" => "hello"} end end ``` ### When NOT to Use **Don't use this when:** You need to test WebSocket transport behavior (reconnection, heartbeats, encoding), or when testing client-side JavaScript channel logic. **Over-application example:** ```elixir # Bad: Using ChannelTest to test transport-level reconnection test "client reconnects after disconnect" do {:ok, _, socket} = socket(MyAppWeb.UserSocket, "user:1", %{user_id: 1}) |> subscribe_and_join(MyAppWeb.RoomChannel, "room:lobby") # ChannelTest doesn't simulate transport disconnections # This tests nothing about actual reconnection behavior Process.exit(socket.channel_pid, :kill) # ... can't meaningfully test reconnect here end ``` **Better alternative:** ```elixir # Use a real integration test with a WebSocket client for transport testing defmodule MyAppWeb.ReconnectionIntegrationTest do use ExUnit.Case @tag :integration test "client reconnects and rejoins after server restart" do # Use a real WS client library for transport-level testing {:ok, client} = PhoenixClient.connect("ws://localhost:4002/socket/websocket") {:ok, _} = PhoenixClient.join(client, "room:lobby") # Simulate disconnect and verify reconnection PhoenixClient.disconnect(client) assert {:ok, _} = PhoenixClient.reconnect(client, timeout: 5000) end end ``` **Why:** ChannelTest exercises your server-side channel logic (join, handle_in, handle_info) in isolation. Transport concerns (WebSocket frames, heartbeats, reconnection) are a separate layer tested separately — mixing them makes tests slow and flaky.