Spec-Zone.ru › Phoenix

JSON и API

Исходный код

Требование: предполагается, что вы ознакомились с вводными руководствами и запустили приложение Phoenix и убедились, что оно работает.

Требование: предполагается, что вы ознакомились с руководством по контроллерам.

С помощью Phoenix Framework также можно создавать веб-API. По умолчанию Phoenix поддерживает JSON, но вы можете использовать любой другой формат представления данных.

JSON API

В этом руководстве мы создадим простой JSON API для хранения наших любимых ссылок. Он будет поддерживать все операции CRUD (создание, чтение, обновление и удаление) из коробки.

Для этого руководства мы воспользуемся генераторами Phoenix, чтобы создать структуру нашего API:

mix phx.gen.json Urls Url urls link:string title:string
* creating lib/hello_web/controllers/url_controller.ex
* creating lib/hello_web/controllers/url_json.ex
* creating lib/hello_web/controllers/changeset_json.ex
* creating test/hello_web/controllers/url_controller_test.exs
* creating lib/hello_web/controllers/fallback_controller.ex
* creating lib/hello/urls/url.ex
* creating priv/repo/migrations/20221129120234_create_urls.exs
* creating lib/hello/urls.ex
* injecting lib/hello/urls.ex
* creating test/hello/urls_test.exs
* injecting test/hello/urls_test.exs
* creating test/support/fixtures/urls_fixtures.ex
* injecting test/support/fixtures/urls_fixtures.ex

Мы разделим эти файлы на четыре категории:

  • Файлы в lib/hello_web отвечают за непосредственное отображение JSON
  • Файлы в lib/hello отвечают за определение контекста и логики сохранения ссылок в базе данных
  • Файлы в priv/repo/migrations отвечают за обновление базы данных
  • Файлы в test предназначены для тестирования контроллеров и контекстов

В этом руководстве мы рассмотрим только первую категорию файлов. Чтобы узнать больше о том, как Phoenix хранит данные и управляет ими, ознакомьтесь с руководством по Ecto и руководством по контекстам. У нас также есть целый раздел, посвящённый тестированию.

В конце генератор предлагает добавить ресурс /url в область :api в lib/hello_web/router.ex:

scope "/api", HelloWeb do
  pipe_through :api
  resources "/urls", UrlController, except: [:new, :edit]
end

Для API используется конвейер :api, который выполняет определённые шаги, например проверяет, что клиент может обрабатывать ответы в формате JSON.

Затем нужно обновить репозиторий, выполнив миграции:

$ mix ecto.migrate

Проверка JSON API

Прежде чем вносить изменения в эти файлы, давайте посмотрим, как работает наш API из командной строки.

Сначала запустим сервер:

$ mix phx.server

Затем проверим, что наш API работает:

$ curl -i http://localhost:4000/api/urls

Если всё прошло по плану, мы должны получить ответ 200:

HTTP/1.1 200 OK
cache-control: max-age=0, private, must-revalidate
content-length: 11
content-type: application/json; charset=utf-8
date: Fri, 06 May 2022 21:22:42 GMT
server: Cowboy
x-request-id: Fuyg-wMl4S-hAfsAAAUk

{"data":[]}

Данные не вернулись, поскольку мы ещё ничего не добавили в базу данных. Давайте добавим несколько ссылок:

$ curl -iX POST http://localhost:4000/api/urls \
   -H 'Content-Type: application/json' \
   -d '{"url": {"link":"https://phoenixframework.org", "title":"Phoenix Framework"}}'

$ curl -iX POST http://localhost:4000/api/urls \
   -H 'Content-Type: application/json' \
   -d '{"url": {"link":"https://elixir-lang.org", "title":"Elixir"}}'

Теперь можно получить все ссылки:

$ curl -i http://localhost:4000/api/urls

Или получить ссылку по её id:

$ curl -i http://localhost:4000/api/urls/1

Затем обновим ссылку:

$ curl -iX PUT http://localhost:4000/api/urls/2 \
   -H 'Content-Type: application/json' \
   -d '{"url": {"title":"Elixir Programming Language"}}'

Ответом должен быть 200 с обновлённой ссылкой в теле.

Наконец, попробуем удалить ссылку:

$ curl -iX DELETE http://localhost:4000/api/urls/2 \
   -H 'Content-Type: application/json'

Для подтверждения успешного удаления ссылки должен вернуться ответ 204.

Формирование JSON

Чтобы понять, как формировать JSON, начнём с действия index из UrlController, определённого в lib/hello_web/controllers/url_controller.ex:

  def index(conn, _params) do
    urls = Urls.list_urls()
    render(conn, :index, urls: urls)
  end

Как видим, это ничем не отличается от того, как Phoenix отображает HTML-шаблоны. Мы вызываем render/3, передавая соединение, шаблон, который должны отображать наши представления (:index), и данные, которые нужно сделать доступными для представлений.

Обычно в Phoenix для каждого формата представления используется отдельное представление. Для отображения HTML мы использовали бы UrlHTML. Теперь, когда мы формируем JSON, рядом с шаблоном в lib/hello_web/controllers/url_json.ex найдём представление UrlJSON. Давайте откроем его:

defmodule HelloWeb.UrlJSON do
  alias Hello.Urls.Url

  @doc """
  Renders a list of urls.
  """
  def index(%{urls: urls}) do
    %{data: for(url <- urls, do: data(url))}
  end

  @doc """
  Renders a single url.
  """
  def show(%{url: url}) do
    %{data: data(url)}
  end

  defp data(%Url{} = url) do
    %{
      id: url.id,
      link: url.link,
      title: url.title
    }
  end
end

Это представление очень простое. Функция index получает все URL-адреса и преобразует их в список отображений. Эти отображения помещаются в ключ data в корне, точно так же, как мы видели при обращении к приложению через cURL. Иными словами, наше JSON-представление преобразует сложные данные в простые структуры данных Elixir. После того как слой представлений возвращает результат, Phoenix использует библиотеку Jason, чтобы закодировать JSON и отправить ответ клиенту.

Изучив оставшуюся часть контроллера, вы узнаете, что действие show похоже на действие index. Для действий create, update и delete Phoenix использует ещё одну важную возможность — «резервный обработчик действия» (Action fallback).

Чтение данных запроса

Как мы уже видели в разделе Жизненный цикл запроса, все действия контроллера принимают два аргумента: conn и params. Plug автоматически разбирает параметры пути, параметры запроса и тело запроса и помещает их в аргумент params.

Минимальный пример

Рассмотрим минимальную конфигурацию с тремя маршрутами API (lib/hello_web/router.ex):

defmodule HelloWeb.Router do
  use HelloWeb, :router

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

  scope "/api", HelloWeb do
    pipe_through :api

    get "/", HelloController, :show
    get "/:name", HelloController, :show
    post "/", HelloController, :show
  end
end

Контроллер с одним действием (lib/hello_web/controllers/hello_controller.ex):

defmodule HelloWeb.HelloController do
  use HelloWeb, :controller

  def show(conn, params) do
    render(conn, :greet, params)
  end
end

и JSON-представление (lib/hello_web/controllers/hello_json.ex):

defmodule HelloWeb.HelloJSON do
  def greet(%{"name" => name}) do
    %{greeting: "Hello #{name}!"}
  end
end

Эта простая конфигурация может обрабатывать все следующие запросы:

$ curl http://localhost:4000/api/world

$ curl http://localhost:4000/api?name=world

$ curl -iX POST http://localhost:4000/api \
   -H 'Content-Type: application/json' \
   -d '{"name": "world"}'

Совершенно одинаково: Hello world!

Данные запроса объединяются

Поскольку все данные объединяются в одну карту, передача данных из нескольких источников (например, параметра пути и тела JSON или параметра запроса и формы) приведёт к тому, что поля с одинаковыми именами будут перезаписывать друг друга.

Приоритет следующий: Path Parameters > Body (any kind) > Query Parameters

Продолжим предыдущий пример и добавим обработчик ещё одного POST-запроса, принимающий параметр пути:

  scope "/api", HelloWeb do
    pipe_through :api

    get "/", HelloController, :show
    get "/:name", HelloController, :show
    post "/", HelloController, :show
    post "/:name", HelloController, :show # New route
    #      ^^^^^ New parameter
  end

Теперь рассмотрим следующие запросы:

$ curl -iX POST http://localhost:4000/api/name1?name=name3 \
   -H 'Content-Type: application/json' \
   -d '{"name": "name2"}'

$ curl -iX POST http://localhost:4000/api?name=name3 \
   -H 'Content-Type: application/json' \
   -d '{"name": "name2"}'

$ curl -iX POST http://localhost:4000/api?name=name3

Они вернут соответственно Hello name1! (параметр пути), Hello name2! (тело запроса) и Hello name3! (параметр запроса). При необходимости к этим параметрам можно обращаться по отдельности через conn.path_params, conn.body_params и conn.query_params соответственно.

Резервный обработчик действия

Резервный обработчик действия позволяет централизовать обработку ошибок в plug-модулях, которые вызываются, если действие контроллера не возвращает структуру %Plug.Conn{}. Эти plug-модули получают как conn, изначально переданное действию контроллера, так и возвращаемое значение действия.

Предположим, у нас есть действие show, которое использует with, чтобы получить запись блога, а затем проверить, разрешено ли текущему пользователю просматривать её. В этом примере можно ожидать, что fetch_post/1 вернёт {:error, :not_found}, если запись не найдена, а authorize_user/3 вернёт {:error, :unauthorized}, если у пользователя нет прав. Для надлежащей обработки таких ошибок можно использовать представления ErrorHTML и ErrorJSON, которые Phoenix создаёт для каждого нового приложения:

defmodule HelloWeb.MyController do
  use Phoenix.Controller

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- fetch_post(id),
         :ok <- authorize_user(current_user, :view, post) do
      render(conn, :show, post: post)
    else
      {:error, :not_found} ->
        conn
        |> put_status(:not_found)
        |> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
        |> render(:"404")

      {:error, :unauthorized} ->
        conn
        |> put_status(403)
        |> put_view(html: HelloWeb.ErrorHTML, json: HelloWeb.ErrorJSON)
        |> render(:"403")
    end
  end
end

Теперь представьте, что похожую логику нужно реализовать для каждого контроллера и действия, обрабатываемых API. Это привело бы к большому количеству повторяющегося кода.

Вместо этого можно определить plug-модуль, который умеет обрабатывать именно эти случаи ошибок. Поскольку контроллеры являются plug-модулями, определим наш plug как контроллер:

defmodule HelloWeb.MyFallbackController do
  use Phoenix.Controller

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(json: HelloWeb.ErrorJSON)
    |> render(:"404")
  end

  def call(conn, {:error, :unauthorized}) do
    conn
    |> put_status(403)
    |> put_view(json: HelloWeb.ErrorJSON)
    |> render(:"403")
  end
end

Затем можно указать новый контроллер в качестве action_fallback и просто удалить блок else из with:

defmodule HelloWeb.MyController do
  use Phoenix.Controller

  action_fallback HelloWeb.MyFallbackController

  def show(conn, %{"id" => id}, current_user) do
    with {:ok, post} <- fetch_post(id),
         :ok <- authorize_user(current_user, :view, post) do
      render(conn, :show, post: post)
    end
  end
end

Если условия with не совпадут, HelloWeb.MyFallbackController получит исходный conn, а также результат действия и сформирует соответствующий ответ.

FallbackController и ChangesetJSON

Теперь, зная это, рассмотрим FallbackController (lib/hello_web/controllers/fallback_controller.ex), созданный mix phx.gen.json. В частности, он обрабатывает один вариант (второй сгенерирован в качестве примера):

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(json: HelloWeb.ChangesetJSON)
    |> render(:error, changeset: changeset)
  end

Этот вариант предназначен для обработки возвращаемых типов {:error, changeset} из контекста HelloWeb.Urls и преобразования их в отображаемые ошибки с помощью представления ChangesetJSON. Давайте откроем lib/hello_web/controllers/changeset_json.ex, чтобы узнать больше:

defmodule HelloWeb.ChangesetJSON do
  @doc """
  Renders changeset errors.
  """
  def error(%{changeset: changeset}) do
    # When encoded, the changeset returns its errors
    # as a JSON object. So we just pass it forward.
    %{errors: Ecto.Changeset.traverse_errors(changeset, &translate_error/1)}
  end
end

Как видим, ошибки преобразуются в структуру данных, которая будет отображена в формате JSON. Changeset — это структура данных, отвечающая за приведение типов и проверку данных. В нашем примере она определена в Hello.Urls.Url.changeset/1. Давайте откроем lib/hello/urls/url.ex и посмотрим на её определение:

  @doc false
  def changeset(url, attrs) do
    url
    |> cast(attrs, [:link, :title])
    |> validate_required([:link, :title])
  end

Как видите, changeset требует указать и ссылку, и заголовок. Поэтому можно попробовать отправить URL без ссылки и заголовка и посмотреть, как ответит API:

$ curl -iX POST http://localhost:4000/api/urls \
   -H 'Content-Type: application/json' \
   -d '{"url": {}}'

{"errors": {"link": ["can't be blank"], "title": ["can't be blank"]}}

Попробуйте изменить функцию changeset и посмотреть, как поведёт себя API.

Приложения только для API

Если вы хотите создать приложение Phoenix исключительно для API, при вызове mix phx.new можно передать несколько параметров. Давайте посмотрим, какие флаги --no-* нужно использовать, чтобы не создавать ненужные для REST API части приложения Phoenix.

Выполните в терминале:

$ mix help phx.new

В выводе должно быть следующее:

  • --no-assets - equivalent to --no-esbuild and --no-tailwind
  • --no-dashboard - do not include Phoenix.LiveDashboard
  • --no-ecto - do not generate Ecto files
  • --no-esbuild - do not include esbuild dependencies and
    assets. We do not recommend setting this option, unless for API
    only applications, as doing so requires you to manually add and
    track JavaScript dependencies
  • --no-gettext - do not generate gettext files
  • --no-html - do not generate HTML views
  • --no-live - comment out LiveView socket setup in your Endpoint
    and assets/js/app.js. Automatically disabled if --no-html is given
  • --no-mailer - do not generate Swoosh mailer files
  • --no-tailwind - do not include tailwind dependencies and
    assets. The generated markup will still include Tailwind CSS
    classes, those are left-in as reference for the subsequent
    styling of your layout and components

При создании приложения Phoenix для API очевидно следует использовать --no-html, чтобы не включать ненужную инфраструктуру для HTML. Также можно передать --no-assets, если вам не нужны средства управления ресурсами, --no-gettext, если вы не используете интернационализацию, и так далее.

Итак, чтобы создать простое API под названием Hello без интерфейса, подключения к базе данных, интернационализации и почтовой службы, можно передать следующие флаги:

$ mix phx.new hello --no-assets --no-dashboard --no-ecto --no-gettext --no-html --no-mailer

По умолчанию при этом всё ещё включаются модули Telemetry, PubSub и DNS Cluster. При необходимости их можно удалить вручную.

Также учтите, что ничто не мешает создать серверную часть, одновременно поддерживающую REST API и веб-приложение (HTML, ресурсы, интернационализацию и сокеты).

← Предыдущая страница Ecto
Следующая страница → LiveView

Пакет Hex Предпросмотр Hex (текущий файл) Скачать версию ePub

Создано с помощью ExDoc (v0.40.3) для языка программирования Elixir

© 2014 Chris McCord
Licensed under the MIT License.
https://phoenix.hexdocs.pm/json_and_apis.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API