phoenix-api-channels

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Phoenix APIs, Channels, and Presence (Elixir/BEAM)

Phoenix API、Channels 和 Presence(Elixir/BEAM)

Phoenix excels at REST/JSON APIs and WebSocket Channels with minimal boilerplate, leveraging the BEAM for fault tolerance, lightweight processes, and supervised PubSub/Presence.
Core pillars
  • Controllers for JSON APIs with plugs, pipelines, and versioning.
  • Contexts own data (Ecto schemas + queries) and expose a narrow API to controllers/channels.
  • Channels + PubSub for fan-out real-time updates; Presence for tracking users/devices.
  • Auth via plugs (session/cookie for browser, token/Bearer for APIs), with signed params.

Phoenix 在 REST/JSON API 和 WebSocket Channels 方面表现出色,所需样板代码极少,借助 BEAM 实现容错、轻量级进程以及受监管的 PubSub/Presence。
核心支柱
  • 用于 JSON API 的控制器,支持插件(plugs)、管道(pipelines)和版本控制。
  • 上下文(Contexts)负责数据管理(Ecto 模式 + 查询),并向控制器/Channels 暴露精简的 API。
  • Channels + PubSub 用于实时更新的扇出分发;Presence 用于跟踪用户/设备。
  • 通过插件实现认证(浏览器使用会话/ Cookie,API 使用令牌/Bearer),支持签名参数。

Project Setup

项目设置

bash
mix phx.new my_api --no-html --no-live
cd my_api
mix deps.get
mix ecto.create
mix phx.server
Key files:
  • lib/my_api_web/endpoint.ex
    — plugs, sockets, instrumentation
  • lib/my_api_web/router.ex
    — pipelines, scopes, versioning, sockets
  • lib/my_api_web/controllers/*
    — REST/JSON controllers
  • lib/my_api/*
    — contexts + Ecto schemas (ownership of data logic)
  • lib/my_api_web/channels/*
    — Channel modules

bash
mix phx.new my_api --no-html --no-live
cd my_api
mix deps.get
mix ecto.create
mix phx.server
关键文件:
  • lib/my_api_web/endpoint.ex
    — 插件、套接字、埋点监控
  • lib/my_api_web/router.ex
    — 管道、作用域、版本控制、套接字
  • lib/my_api_web/controllers/*
    — REST/JSON 控制器
  • lib/my_api/*
    — 上下文 + Ecto 模式(数据逻辑的归属)
  • lib/my_api_web/channels/*
    — Channel 模块

Routing and Pipelines

路由与管道

Separate browser vs API pipelines; version APIs with scopes.
elixir
defmodule MyApiWeb.Router do
  use MyApiWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
    plug :protect_from_forgery
    plug MyApiWeb.Plugs.RequireAuth
  end

  scope "/api", MyApiWeb do
    pipe_through :api

    scope "/v1", V1, as: :v1 do
      resources "/users", UserController, except: [:new, :edit]
      post "/sessions", SessionController, :create
    end
  end

  socket "/socket", MyApiWeb.UserSocket,
    websocket: [connect_info: [:peer_data, :x_headers]],
    longpoll: false
end
Tips
  • Keep pipelines short; push auth/guards into plugs.
  • Expose
    socket "/socket"
    for Channels; restrict transports as needed.

分离浏览器与 API 管道;通过作用域实现 API 版本控制。
elixir
defmodule MyApiWeb.Router do
  use MyApiWeb, :router

  pipeline :api do
    plug :accepts, ["json"]
    plug :fetch_session
    plug :protect_from_forgery
    plug MyApiWeb.Plugs.RequireAuth
  end

  scope "/api", MyApiWeb do
    pipe_through :api

    scope "/v1", V1, as: :v1 do
      resources "/users", UserController, except: [:new, :edit]
      post "/sessions", SessionController, :create
    end
  end

  socket "/socket", MyApiWeb.UserSocket,
    websocket: [connect_info: [:peer_data, :x_headers]],
    longpoll: false
end
提示
  • 保持管道精简;将认证/守卫逻辑移至插件中。
  • 暴露
    socket "/socket"
    以支持 Channels;根据需要限制传输方式。

Controllers and Plugs

控制器与插件

Controllers stay thin; contexts own the logic.
elixir
defmodule MyApiWeb.V1.UserController do
  use MyApiWeb, :controller
  alias MyApi.Accounts

  action_fallback MyApiWeb.FallbackController

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

  def create(conn, params) do
    with {:ok, user} <- Accounts.register_user(params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", ~p\"/api/v1/users/#{user.id}\")
      |> render(:show, user: user)
    end
  end
end
FallbackController centralizes error translation (
{:error, :not_found}
→ 404 JSON).
Plugs
  • RequireAuth
    verifies bearer/session tokens, sets
    current_user
    .
  • Use
    plug :scrub_params
    -style transforms in pipelines, not controllers.
  • Avoid heavy work in plugs; they run per-request.

控制器保持精简;数据逻辑由上下文负责。
elixir
defmodule MyApiWeb.V1.UserController do
  use MyApiWeb, :controller
  alias MyApi.Accounts

  action_fallback MyApiWeb.FallbackController

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

  def create(conn, params) do
    with {:ok, user} <- Accounts.register_user(params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", ~p\"/api/v1/users/#{user.id}\")
      |> render(:show, user: user)
    end
  end
end
FallbackController 集中处理错误转换(例如将
{:error, :not_found}
转换为 404 JSON 响应)。
插件
  • RequireAuth
    验证 Bearer/会话令牌,设置
    current_user
  • 在管道中使用
    plug :scrub_params
    这类转换逻辑,而非控制器中。
  • 避免在插件中执行繁重操作;它们会在每个请求时运行。

Contexts and Data (Ecto)

上下文与数据(Ecto)

Contexts expose only what controllers/channels need.
elixir
defmodule MyApi.Accounts do
  import Ecto.Query, warn: false
  alias MyApi.{Repo, Accounts.User}

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

  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end
end
Guidelines
  • Keep schema modules free of controller knowledge.
  • Validate at the changeset; use
    Ecto.Multi
    for multi-step operations.
  • Prefer pagination helpers (
    Scrivener
    ,
    Flop
    ) for large lists.

上下文仅暴露控制器/Channels 所需的内容。
elixir
defmodule MyApi.Accounts do
  import Ecto.Query, warn: false
  alias MyApi.{Repo, Accounts.User}

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

  def register_user(attrs) do
    %User{}
    |> User.registration_changeset(attrs)
    |> Repo.insert()
  end
end
指导原则
  • 模式模块中不要包含控制器相关逻辑。
  • 在 changeset 中进行验证;使用
    Ecto.Multi
    处理多步骤操作。
  • 对于大型列表,优先使用分页助手(如
    Scrivener
    Flop
    )。

Channels, PubSub, and Presence

Channels、PubSub 与 Presence

Channel module example:
elixir
defmodule MyApiWeb.RoomChannel do
  use Phoenix.Channel
  alias Phoenix.Presence

  def join("room:" <> room_id, _payload, socket) do
    send(self(), :after_join)
    {:ok, assign(socket, :room_id, room_id)}
  end

  def handle_info(:after_join, socket) do
    Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)})
    push(socket, "presence_state", Presence.list(socket))
    {:noreply, socket}
  end

  def handle_in("message:new", %{"body" => body}, socket) do
    broadcast!(socket, "message:new", %{user_id: socket.assigns.user_id, body: body})
    {:noreply, socket}
  end
end
PubSub from contexts
elixir
def create_order(attrs) do
  with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
    Phoenix.PubSub.broadcast(MyApi.PubSub, "orders", {:order_created, order})
    {:ok, order}
  end
end
Best practices
  • Authorize in
    UserSocket.connect/3
    before joining topics.
  • Limit payload sizes; validate incoming events.
  • Use topic partitioning for tenancy (
    "tenant:" <> tenant_id <> ":room:" <> room_id
    ).

Channel 模块示例:
elixir
defmodule MyApiWeb.RoomChannel do
  use Phoenix.Channel
  alias Phoenix.Presence

  def join("room:" <> room_id, _payload, socket) do
    send(self(), :after_join)
    {:ok, assign(socket, :room_id, room_id)}
  end

  def handle_info(:after_join, socket) do
    Presence.track(socket, socket.assigns.user_id, %{online_at: System.system_time(:second)})
    push(socket, "presence_state", Presence.list(socket))
    {:noreply, socket}
  end

  def handle_in("message:new", %{"body" => body}, socket) do
    broadcast!(socket, "message:new", %{user_id: socket.assigns.user_id, body: body})
    {:noreply, socket}
  end
end
从上下文触发 PubSub
elixir
def create_order(attrs) do
  with {:ok, order} <- %Order{} |> Order.changeset(attrs) |> Repo.insert() do
    Phoenix.PubSub.broadcast(MyApi.PubSub, "orders", {:order_created, order})
    {:ok, order}
  end
end
最佳实践
  • 在加入主题前,在
    UserSocket.connect/3
    中完成授权。
  • 限制 payload 大小;验证传入事件。
  • 对于多租户场景,使用主题分区(例如
    "tenant:" <> tenant_id <> ":room:" <> room_id
    )。

Authentication Patterns

认证模式

  • API tokens: Accept
    authorization: Bearer <token>
    ; verify in plug, assign
    current_user
    .
  • Signed params:
    Phoenix.Token.sign/verify
    for short-lived join params.
  • Rate limiting: Use plugs + ETS/Cachex or reverse proxy (NGINX/Cloudflare).
  • CORS: Configure in
    Endpoint
    with
    cors_plug
    .

  • API 令牌:接收
    authorization: Bearer <token>
    ;在插件中验证并设置
    current_user
  • 签名参数:使用
    Phoenix.Token.sign/verify
    生成短期的加入参数。
  • 速率限制:使用插件 + ETS/Cachex 或反向代理(如 NGINX/Cloudflare)实现。
  • CORS:在
    Endpoint
    中通过
    cors_plug
    配置。

Testing

测试

Use generated helpers:
elixir
defmodule MyApiWeb.UserControllerTest do
  use MyApiWeb.ConnCase, async: true

  test "lists users", %{conn: conn} do
    conn = get(conn, ~p\"/api/v1/users\")
    assert json_response(conn, 200)["data"] == []
  end
end
Channel tests:
elixir
defmodule MyApiWeb.RoomChannelTest do
  use MyApiWeb.ChannelCase, async: true

  test "broadcasts messages" do
    {:ok, _, socket} = connect(MyApiWeb.UserSocket, %{"token" => "abc"})
    {:ok, _, socket} = subscribe_and_join(socket, "room:123", %{})
    ref = push(socket, "message:new", %{"body" => "hi"})
    assert_reply ref, :ok
    assert_broadcast "message:new", %{body: "hi"}
  end
end
DataCase: isolates DB per test; use fixtures/factories for setup.

使用生成的助手函数:
elixir
defmodule MyApiWeb.UserControllerTest do
  use MyApiWeb.ConnCase, async: true

  test "lists users", %{conn: conn} do
    conn = get(conn, ~p\"/api/v1/users\")
    assert json_response(conn, 200)["data"] == []
  end
end
Channel 测试:
elixir
defmodule MyApiWeb.RoomChannelTest do
  use MyApiWeb.ChannelCase, async: true

  test "broadcasts messages" do
    {:ok, _, socket} = connect(MyApiWeb.UserSocket, %{"token" => "abc"})
    {:ok, _, socket} = subscribe_and_join(socket, "room:123", %{})
    ref = push(socket, "message:new", %{"body" => "hi"})
    assert_reply ref, :ok
    assert_broadcast "message:new", %{body: "hi"}
  end
end
DataCase:为每个测试隔离数据库;使用 fixtures/factories 进行测试准备。

Telemetry, Observability, and Ops

遥测、可观测性与运维

  • :telemetry
    events from endpoint, controller, channel, and Ecto queries; export via
    OpentelemetryPhoenix
    and
    OpentelemetryEcto
    .
  • Use
    Plug.Telemetry
    for request metrics; add logging metadata (request_id, user_id).
  • Releases:
    MIX_ENV=prod mix release
    ; configure runtime in
    config/runtime.exs
    .
  • Clustering:
    libcluster
    + distributed PubSub for multi-node Presence.
  • Assetless APIs: disable unused watchers (esbuild/tailwind) for API-only apps.

  • 端点、控制器、Channel 和 Ecto 查询会产生
    :telemetry
    事件;可通过
    OpentelemetryPhoenix
    OpentelemetryEcto
    导出。
  • 使用
    Plug.Telemetry
    收集请求指标;添加日志元数据(如 request_id、user_id)。
  • 发布版本:执行
    MIX_ENV=prod mix release
    ;在
    config/runtime.exs
    中配置运行时参数。
  • 集群部署:使用
    libcluster
    + 分布式 PubSub 实现多节点 Presence。
  • 无资产 API:对于仅 API 应用,禁用未使用的监视器(如 esbuild/tailwind)。

Common Pitfalls

常见陷阱

  • Controllers doing queries directly instead of delegating to contexts.
  • Not authorizing in
    UserSocket.connect/3
    , leading to topic exposure.
  • Missing
    action_fallback
    → inconsistent error shapes.
  • Forgetting to limit event payloads; large messages can overwhelm channels.
  • Leaving longpoll enabled when unused; disable to reduce surface area.
Phoenix API + Channels shine when contexts own data, controllers stay thin, and Channels use PubSub/Presence with strict authorization and telemetry. The BEAM handles concurrency and fault tolerance; focus on clear boundaries and real-time experiences.
  • 控制器直接执行查询,而非委托给上下文。
  • 未在
    UserSocket.connect/3
    中进行授权,导致主题暴露。
  • 缺少
    action_fallback
    ,导致错误响应格式不一致。
  • 忘记限制事件 payload 大小;大消息可能导致 Channel 过载。
  • 在未使用长轮询时仍保持其启用状态;应禁用以减少攻击面。
当上下文负责数据管理、控制器保持精简,且 Channels 结合严格授权与遥测使用 PubSub/Presence 时,Phoenix API + Channels 的优势将充分发挥。BEAM 负责处理并发与容错;开发者只需专注于清晰的边界划分和实时体验的构建。