mix phx.gen.jsonを使わず気軽に作れるPhoenix API

[fukuoka.ex](https://fukuokaex.connpass.com/)代表のpiacereです
ご覧いただいて、ありがとうございます :bow:

Phoenixは、mix phx.gen.jsonで、DBアクセスをラップするREST APIが自動生成できますが、DB利用の無い、任意のAPIを作るときの作法が分かりにくいため、最小限のAPIを手動で作成するサンプルを挙げておきます

PhoenixによるAPIの構築が、とてもシンプルで、かつAPI返却は、どの言語よりも直感的だと言うことをご理解いただける内容です

なお、[「Phoenix」](https://qiita.com/piacerex/items/b7787580fce5f148242f#phoenix%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)は、ElixirのWebフレームワークです

内容が、面白かったり、気に入ったら、「いいね」よろしくお願いします :wink:

# API作成対象となるPhoenix PJの作成

まずは、まっさらなPhoenix PJにAPIを追加する手順で説明するので、Phoenix PJを作成してください

```
mix phx.new api_sample --no-ecto
　…（ファイル作成ログが続く）…
Fetch and install dependencies? [Yn] （←y、Enterを入力）
```

Phoenixを起動します

```
cd api_sample 
iex -S mix phx.server
```

ブラウザで「```http://localhost:4000```」にアクセスすると、以下ページが表示されることを確認します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/6f0e6970-5536-2af3-3bb9-50b07bf26ba3.png)

# APIリクエスト受付コントローラファイルを追加

以下のコントローラファイルを追加します

このコントローラのindexという関数で、APIリクエストを受付できます

なお、このindexという関数名は、後述するルーティングと合わせていれば、どんな関数名でもOKです

中で呼び出しているrenderで、レスポンス返却処理を行います

```elixir:lib/api_sample_web/controllers/api_controller.ex
defmodule ApiSampleWeb.ApiController do
	use ApiSampleWeb, :controller

	def index( conn, params ) do
		render( conn, "api.json", api_data: params )
	end
end
```

# APIレスポンス返却ビューファイルを追加

以下のビューファイルを追加します

このビューのrenderという関数で、APIレスポンスを返却できます

なお、このrenderという関数名は、Phoenixのビルトイン関数のため、関数名は変更できません（コントローラから呼び出されている引数3つのrenderから、ここで定義する引数2つのrenderがコールバックされます）

また、第一引数の「api.json」と、第二引数の「%{ api_data: _data }」のキーである「api_data」は、上記コントローラのrender呼出の第二引数および第三引数のキーと一致させる必要があります（逆に言えば、両方を書き換えてしまえば良い、ということです）

render内に、返却するデータを記述します

```elixir:lib/api_sample_web/views/api_view.ex
defmodule ApiSampleWeb.ApiView do
	use ApiSampleWeb, :view

	def render( "api.json", %{ api_data: _params } ) do
		%{
			id:   123, 
			name: "hoge"
		}
	end
end
```

Elixirのマップ（もしくはマップリスト）が、そのままJSON化されるので、どの言語よりも、直感的な記述でグッドです

# API用エントリーポイントをルーティングに追加

APIのためのエントリーポイントをrouter.exに追加します

```elixir:lib/api_sample_web/router.ex
defmodule ApiSampleWeb.Router do
…
  scope "/", ApiSampleWeb do
    pipe_through :browser

    get "/", PageController, :index
  
    get  "/api", ApiController, :index  # ←ここを追加
end
```

# 最小限のAPIを動作確認する

ブラウザ（かRESTクライアント）で、「```http://localhost:4000/api```」にアクセスすると、以下のように、APIからの返却が確認できます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/76bdf043-9282-9dd6-9b96-335caf982ded.png)
ビューのrenderをいじって、返却内容が変わることを確認してみてください

ここまでが、Phoenixで、最小限のAPIを構築する手順となります

とても簡単で、シンプルな構造だと言うことが、お分かりいただけたのでは無いかと思います

# APIリクエストで指定されたパラメータを返却に含める

コントローラが受けたパラメータは、既にrenderに渡しているので、ビューを以下のように修正すれば、paramsにパラメータが入るようになります

```elixir:lib/api_sample_web/views/api_view.ex
defmodule ApiSampleWeb.ApiView do
	use ApiSampleWeb, :view

	def render( "api.json", %{ api_data: params } ) do
		%{
			params: params, 
			id:     123, 
			name:   "hoge"
		}
	end
end
```

ブラウザ（かRESTクライアント）をリロードすると、空のparamsが出てきます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/b16ae198-863c-0222-6224-e1977d50f752.png)
ブラウザ（かRESTクライアント）のURLを、「```http://localhost:4000/api?media=Twitter&count=100```」に変更して、アクセスすると、params配下にURLで指定したパラメータが出てきます
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/3b24de41-2567-e0c6-9469-0451ad1b653a.png)


# 自前のPhoenix PJにAPIを追加するには？

上記2ファイルの「ApiSample」を、既存のPhoenix PJ名に置換し、配置した後、router.exに上記同様のエントリー追加をすればOKです

# オマケ：mix phx.gen.jsonで生成される内容の解説

基本は、ここまでで終わりですが、mix phx.gen.jsonで生成されるコントローラ／ビューについても、少し解説しておきます

対象は、Elixir／Phoenix入門チュートリアルの第6回、[Vue.js＋内部API（表示編）](https://qiita.com/piacere_ex/items/50d847170291c41fef64) で生成したコントローラ／ビューとします

まず、コントローラですが、index／create／show／update共に、DBアクセッサに定義されたDB操作関数であるlist_postsやcreate_post等を呼び出した後、上記サンプル同様、renderを呼びます

```elixir:lib/vue_sample_web/controllers/post_controller.ex
defmodule VueSampleWeb.PostController do
…
  def index(conn, _params) do
    posts = Api.list_posts()
    render(conn, "index.json", posts: posts)
  end

  def create(conn, %{"post" => post_params}) do
    with {:ok, %Post{} = post} <- Api.create_post(post_params) do
      conn
      |> put_status(:created)
      |> put_resp_header("location", post_path(conn, :show, post))
      |> render("show.json", post: post)
    end
  end

  def show(conn, %{"id" => id}) do
    post = Api.get_post!(id)
    render(conn, "show.json", post: post)
  end

  def update(conn, %{"id" => id, "post" => post_params}) do
    post = Api.get_post!(id)

    with {:ok, %Post{} = post} <- Api.update_post(post, post_params) do
      render(conn, "show.json", post: post)
    end
  end

  def delete(conn, %{"id" => id}) do
    post = Api.get_post!(id)
    with {:ok, %Post{}} <- Api.delete_post(post) do
      send_resp(conn, :no_content, "")
    end
  end
…
```

router.exでは、「resource」でエントリーポイントが指定されますが、実際に各関数が、どのREST APIにマッピングされているかは、以下mixコマンドで確認できます

```
mix phx.routes

…
post_path  GET     /posts      VueSampleWeb.PostController :index
post_path  GET     /posts/:id  VueSampleWeb.PostController :show
post_path  POST    /posts      VueSampleWeb.PostController :create
post_path  PATCH   /posts/:id  VueSampleWeb.PostController :update
           PUT     /posts/:id  VueSampleWeb.PostController :update
post_path  DELETE  /posts/:id  VueSampleWeb.PostController :delete
```

DBアクセッサは、コンテキスト名（mix phx.gen.jsonの第一引数に指定する文字列）のフォルダ配下に、【コンテキスト名.ex】で生成されるファイルで、以下のように定義されています

```elixir:lib/vue_sample/api/api.ex
defmodule VueSample.Api do
  @moduledoc """
  The Api context.
  """
  …
  def list_posts do
    Repo.all(Post)
  end
  …
  def create_post(attrs \\ %{}) do
    %Post{}
    |> Post.changeset(attrs)
    |> Repo.insert()
  end
  …
```

次にビューですが、1つ目のrenderは、コントローラのindexから、Phoenixコールバック経由で呼ばれます

2つ目のrenderは、コントローラのcreate／show／updateから、Phoenixコールバック経由で呼ばれます

3つ目のrenderは、上記2つのrender内で呼ばれているrender_many（複数データ返却用）およびrender_one（単一データ返却用）からコールバックされる、データ返却用フォーマットです

render_many時は、このフォーマットのリストが返却され、render_one時は、このフォーマットそのものが返却されます

```elixir:lib/vue_sample_web/views/post_view.ex
defmodule VueSampleWeb.PostView do
  use VueSampleWeb, :view
  alias VueSampleWeb.PostView

  def render("index.json", %{posts: posts}) do
    %{data: render_many(posts, PostView, "post.json")}
  end

  def render("show.json", %{post: post}) do
    %{data: render_one(post, PostView, "post.json")}
  end

  def render("post.json", %{post: post}) do
    %{id: post.id,
      title: post.title,
      body: post.body}
  end
end
```

# 終わり

Phoenixにおける最小限のAPIを、手動作成のコントローラ／ビューで作成してみました

更に、mix phx.gen.jsonで生成されるコントローラ／ビューについても解説しました

DB利用の無い任意のAPIを作るときや、mix phx.gen.jsonで生成されたDB利用時のAPIを改造する際の基本が、今回の内容で掴めたのでは無いかと思います

【2019/3/22追記】
以下のコラムで、今回の知識を応用したコラムを書いてます

- [mix phx.gen.jsonで作った既存APIの非DB化](https://qiita.com/piacere_ex/items/bc80fd25e98687fd029f) 
- [既存APIのリネーム①：URL、コントローラおよびビューのモジュール名](https://qiita.com/piacere_ex/items/688b3e2542bc67ef52c7)
- [既存APIのリネーム②：リクエストJSON、レスポンスJSON](https://qiita.com/piacere_ex/items/ff3c95b5600dfc20ca58)

# p.s.「いいね」よろしくお願いします

ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください！:tada: