PhoenixでDB CRUD以外のJSONを返すAPIを作る

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

Phoenixのmix phx.gen.jsonで作成したAPIは、DB（テーブル）と1対1のCRUD APIを自動生成してくれて便利なので、mix phx.gen.jsonした結果を改造して、DB CURD以外のJSONを返すAPIを作る手順をまとめてみます

なお、mix phx.gen.json自体の使い方については、[Elixir入門「第3回：Phoenix 1.3で高速webアプリ ＆ REST APIアプリをサクッと書いてみる」](https://www.slideshare.net/piacere_ex/elixir3phoenix-13web-rest-api-81219622)のP22以降をご覧ください
[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/ec14252c-8486-5925-b3a0-aa222458edda.png)](https://www.slideshare.net/piacere_ex/elixir3phoenix-13web-rest-api-81219622)

# mix phx.gen.jsonが生成するcontroller／viewの構造

上記スライドに挙げている例の通り、mix phx.gen.jsonでAPIを作成します

```
mix phx.gen.json Tools Post posts title:string body:text
```

すると、controllersフォルダ配下に、以下のようなモジュールが作成されます

```elixir:lib/api_web/controllers/post_controller.ex
defmodule ApiWeb.PostController do
  …
  def index(conn, _params) do
    api = Tools.list_posts()
    render(conn, "index.json", api: api)
  end
```

この自動生成されたコントローラは、index()であれば、テーブルデータ全件のマップリストを、render()に渡し、JSONデータとして返却します

なお、index()以外も、だいたい同じ構造となっています

テーブルデータ全件を取得するRepo.all()は、以下のような定義が自動生成されます（index()以外が使うCRUD関数も、このモジュール中に定義されています）

```elixir:lib/api/tools/tools.ex
defmodule Api.Tools do
  …
  def list_posts do
    Repo.all(Post)
  end
  …
```

その後、呼ばれるrender()は、ビューとして自動生成され、3つの関数で構成されます

```elixir:lib/api_web/views/post_view.ex
defmodule ApiWeb.PostView do
  use SampleAnalyticsWeb, :view
  alias SampleAnalyticsWeb.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
```

index()が使うのは、1つ目の関数と、3つ目の関数です

1つ目の関数（index）で使われるrender_many()は、複数件のマップリストとして返却します

2つ目の関数（show）で使われるrender_one()は、単品のマップとして返却します

3つ目の関数は、render_many()とrender_one()のどちらからも呼ばれる、単品マップを返却するためのコールバック的な関数で、ここでDB列を表す構造体を、返却するJSONに相当するマップへと変換しています（render_many()は、この関数を、各行毎に呼び出しています）

# DB CURD以外のJSONを返却するAPIを作る

DB CURD以外のJSONを返却するAPIを作るためには、コントローラにDB CRUD以外の任意の処理をさせ、ビューのrender()では、DB列の構造体で無いものをマップとして返せば良い、ということです

まず、コントローラからrender()に渡すデータを変更します

下記の例では、元々あったテーブルデータ全件を返却する代わりに、Excelionを使って、Excelデータを読み込み、そのマップリストを返却しています（なお、Excelは、1行目に列ヘッダーがあるデータを想定しており、1行目は読み飛ばしています）

```elixir:lib/api_web/controllers/post_controller.ex
defmodule ApiWeb.PostController do
  …
  def index(conn, _params) do
	api = "sample.xlsx"
		|> Excelion.parse!( 0, 1 )
		|> Enum.drop( 1 )
		|> MapList.zip_atom( &( Enum.zip( [ columns, &1 ] ) ) )
		|> Enum.map( &( &1 |> Enum.reduce( %{}, fn { key, value }, acc -> Map.put( acc, key |> String.to_atom, value ) end ) ) )
    render(conn, "index.json", api: api)
  end
```

ビューのrender()の方は、controllerから渡されたマップリストをそのまま返却すればOKなので、DB列の構造体に置き換えしていた部分を、引数そのままで返却するように変えます

```elixir:lib/api_web/views/post_view.ex
defmodule ApiWeb.PostView do
  …
  def render("post.json", %{post: post}) do
    post
  end
end
```

こんな感じで、DB CURD以外のJSONを返すAPIが作れます

# マップリスト／マップを返せばJSON APIが作られる世界へ

上記のやったことをまとめると、以下の通りです

① コントローラで、任意のマップリスト／マップを作る
② ビューのrender()は引数をそのまま返す

ここには、他言語だと当たり前な、JSONをチマチマ組み立てる処理は、一切、出てきません

にも関わらず、JSON APIがちゃんと定義できてしまう…という、ちょっとした魔法のような出来事です

Elixirの強力なリスト処理をうまく応用した、Phoenixの造りに脱帽です:laughing:

なお、APIのURLを変えたり、モジュール名を変えるコツ、ビューのrender()を使わずに処理する方法などもありますが、これはまた別のコラムにて

# 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: