Elixir:使用 Plug、Cowboy 和 Poison 构建小型 JSON 端点

2025-06-07

Elixir:使用 Plug、Cowboy 和 Poison 构建小型 JSON 端点

很多时候,我只想在应用中添加一个简单的 JSON 端点,以便公开服务或处理 webhook 事件,而无需构建完整的框架。让我们看看使用Plug和Erlang 的Cowboy HTTP 服务器构建一个可用于生产的端点是多么简单。


插头是:

  1. Web 应用程序之间可组合模块的规范
  2. Erlang VM 中不同 Web 服务器的连接适配器

如果你来自Ruby/Rails,想想Rack,来自Node,想想Express等。当然,这些库的概念表面上相似,但它们本身都是独一无二的。

牛仔是:

适用于 Erlang/OTP 的小型、快速且现代的 HTTP 服务器

此外,它是一款支持 HTTP/2 的容错“现代 Web 服务器”,提供一套 WebSocket 处理程序和用于长连接接口。无需赘述,可以肯定地说,它是生产环境的可接受选择。更多信息,请参阅文档。

毒药是:

Elixir 的 JSON 库专注于极快的速度,同时不牺牲简单性完整性正确性

换句话说,它是一个超快、可靠的 JSON 解析库。


构建端点

有了简短的定义,让我们构建一个端点来处理传入的 webhook 事件。现在,我们希望它能够“投入生产”,这对于我们的用例意味着什么?

  1. 容错:始终可用。永远不会宕机(至少不容易宕机 :)
  2. 易于配置:可以部署到任何环境
  3. 经过严格测试:让我们对所运送的货物充满信心

我们确实有一个非常简单的用例,在选择工具和投入时间寻找解决方案之前,了解您自己的需求是一个好主意。


1.创建一个新的、受监督的 Elixir 应用程序:

$ mix new webhook_processor --sup
$ cd webhook_processor
Enter fullscreen mode Exit fullscreen mode

--sup这将创建一个适合用作 OTP 应用程序的应用。OTP 和监管将满足我们上述的首要需求。我们的服务器将受到监管,并在崩溃时自动重启,而服务器可能会崩溃,而 Erlang VM 则不会(至少不容易崩溃 :)。

2. 添加 Plug、Cowboy 和 Poison 作为依赖项

# ./mix.exs
defmodule WebhookProcessor.MixProject do
  use Mix.Project

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

  # Run "mix help compile.app" to learn about applications.
  def application do
    [
      # Add :plug_cowboy to extra_applications
      extra_applications: [:logger, :plug_cowboy],
      mod: {WebhookProcessor.Application, []}
    ]
  end

  # Run "mix help deps" to learn about dependencies.
  defp deps do
    [
      {:plug_cowboy, "~> 2.0"}, # This will pull in Plug AND Cowboy
      {:poison, "~> 3.1"} # Latest version as of this writing
    ]
  end
end
Enter fullscreen mode Exit fullscreen mode

这里需要注意的是,我们在 中添加了plug_cowboy(in deps) 作为 Plug 和 Cowboy 的单一依赖项。我们也需要将其添加到(in ) 的列表:plug_cowboy中。extra_applicationsapplication

3. 混合 deps.get

$ mix deps.get
Enter fullscreen mode Exit fullscreen mode

4. 实现 application.ex

# ./lib/webhook_processor/application.ex
defmodule WebhookProcessor.Application do
  @moduledoc "OTP Application specification for WebhookProcessor"

  use Application

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      # Use Plug.Cowboy.child_spec/3 to register our endpoint as a plug
      Plug.Cowboy.child_spec(
        scheme: :http,
        plug: WebhookProcessor.Endpoint,
        options: [port: 4001]
      )
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: WebhookProcessor.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
Enter fullscreen mode Exit fullscreen mode

5. 实现 WebhookProcessor.Endpoint

# ./lib/webhook_processor/endpoint.ex
defmodule WebhookProcessor.Endpoint do
  @moduledoc """
  A Plug responsible for logging request info, parsing request body's as JSON,
  matching routes, and dispatching responses.
  """

  use Plug.Router

  # This module is a Plug, that also implements it's own plug pipeline, below:

  # Using Plug.Logger for logging request information
  plug(Plug.Logger)
  # responsible for matching routes
  plug(:match)
  # Using Poison for JSON decoding
  # Note, order of plugs is important, by placing this _after_ the 'match' plug,
  # we will only parse the request AFTER there is a route match.
  plug(Plug.Parsers, parsers: [:json], json_decoder: Poison)
  # responsible for dispatching responses
  plug(:dispatch)

  # A simple route to test that the server is up
  # Note, all routes must return a connection as per the Plug spec.
  get "/ping" do
    send_resp(conn, 200, "pong!")
  end

  # Handle incoming events, if the payload is the right shape, process the
  # events, otherwise return an error.
  post "/events" do
    {status, body} =
      case conn.body_params do
        %{"events" => events} -> {200, process_events(events)}
        _ -> {422, missing_events()}
      end

    send_resp(conn, status, body)
  end

  defp process_events(events) when is_list(events) do
    # Do some processing on a list of events
    Poison.encode!(%{response: "Received Events!"})
  end

  defp process_events(_) do
    # If we can't process anything, let them know :)
    Poison.encode!(%{response: "Please Send Some Events!"})
  end

  defp missing_events do
    Poison.encode!(%{error: "Expected Payload: { 'events': [...] }"})
  end

  # A catchall route, 'match' will match no matter the request method,
  # so a response is always returned, even if there is no route to match.
  match _ do
    send_resp(conn, 404, "oops... Nothing here :(")
  end
end
Enter fullscreen mode Exit fullscreen mode

这看起来很多,但该文件的大部分内容只是一些有用的注释。要点是,我们使用getpostPlug.Router生成我们的路由。这个模块本身是一个插件,它定义了自己的插件管道。注意,matchdispatch是我们处理请求和分派响应所必需的。管道在这里是一个关键概念,因为插件的顺序决定了操作的顺序。请注意,匹配在我们定义解析器之前,这意味着除非有路由匹配,否则我们不会解析任何内容。如果顺序颠倒,无论路由是否匹配,我们都会尝试解析请求。有关更多信息,请参阅文档。Plug.Router

6.使端点可配置

# ./lib/webhook_processor/application.ex
defmodule WebhookProcessor.Application do
  @moduledoc "OTP Application specification for WebhookProcessor"

  use Application

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      # Use Plug.Cowboy.child_spec/3 to register our endpoint as a plug
      Plug.Cowboy.child_spec(
        scheme: :http,
        plug: WebhookProcessor.Endpoint,
        # Set the port per environment, see ./config/MIX_ENV.exs
        options: [port: Application.get_env(:webhook_processor, :port)]
      )
    ]

    # See https://hexdocs.pm/elixir/Supervisor.html
    # for other strategies and supported options
    opts = [strategy: :one_for_one, name: WebhookProcessor.Supervisor]
    Supervisor.start_link(children, opts)
  end
end
Enter fullscreen mode Exit fullscreen mode

我们将 Cowboy 选项中硬编码的端口值替换成了环境变量,这样我们就可以在所需的任何环境中运行 webhook 处理器了。最后,为每个MIX_ENV需要的配置创建一个配置文件:

#./config/config.exs

# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

import_config "#{Mix.env()}.exs"

-------------------

# ./config/dev.exs

use Mix.Config

config :webhook_processor, port: 4001

-------------------

# ./config/test.exs

use Mix.Config

config :webhook_processor, port: 4002

-------------------

# ./config/prod.exs

use Mix.Config

config :webhook_processor, port: 80
Enter fullscreen mode Exit fullscreen mode

7. 测试

# ./test/webhook_processor/endpoint_test.exs
defmodule WebhookProcessor.EndpointTest do
  use ExUnit.Case, async: true
  use Plug.Test

  @opts WebhookProcessor.Endpoint.init([])

  test "it returns pong" do
    # Create a test connection
    conn = conn(:get, "/ping")

    # Invoke the plug
    conn = WebhookProcessor.Endpoint.call(conn, @opts)

    # Assert the response and status
    assert conn.state == :sent
    assert conn.status == 200
    assert conn.resp_body == "pong!"
  end

  test "it returns 200 with a valid payload" do
    # Create a test connection
    conn = conn(:post, "/events", %{events: [%{}]})

    # Invoke the plug
    conn = WebhookProcessor.Endpoint.call(conn, @opts)

    # Assert the response
    assert conn.status == 200
  end

  test "it returns 422 with an invalid payload" do
    # Create a test connection
    conn = conn(:post, "/events", %{})

    # Invoke the plug
    conn = WebhookProcessor.Endpoint.call(conn, @opts)

    # Assert the response
    assert conn.status == 422
  end

  test "it returns 404 when no route matches" do
    # Create a test connection
    conn = conn(:get, "/fail")

    # Invoke the plug
    conn = WebhookProcessor.Endpoint.call(conn, @opts)

    # Assert the response
    assert conn.status == 404
  end
end
Enter fullscreen mode Exit fullscreen mode

这些测试非常简单,但它们可以确认我们的服务器是否按预期运行。有人可能会说,我们唯一应该关心的是这些测试的响应代码,而不是处理事件时产生的副作用。除非你编写的是集成式测试,否则测试应该始终在模块边界以内进行,切勿超出范围。


结论

我们几乎不费吹灰之力就构建了一个小巧但功能强大的端点。借助 Cowboy,您可以从一台服务器处理比实际所需更多的连接,因此,我们再将低成本添加到优势列表中。

那么部署呢?让我们来逐步了解如何构建发布版本并部署到 AWS:

  1. 使用 Docker 和 Mix 构建版本
  2. 对 AWS EC2 实例进行地形改造
  3. 使用 Ansible 部署版本

与往常一样,代码可在 GitHub 上获取:https://github.com/jonlunsford/webhook_processor

文章来源:https://dev.to/jonlunsford/elixir-building-a-small-json-endpoint-with-plug-cowboy-and-poison-1826
PREV
玻璃态 CSS 生成器
NEXT
像专业人士一样在应用程序之间重用 React 组件快速设置跟踪和隔离可重用组件定义零配置可重用 React 编译器版本和导出可重用组件在新应用程序中安装组件从使用应用程序中修改组件在第一个应用程序中更新更改(签出)结论