Phoenixお気軽API開発②：Express／Go的なPhoenix軽量APIの上に参照系REST APIを実装する

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

[前作のPhoenix軽量API](https://qiita.com/piacerex/items/144089aa6f2052f4d1cb)は、シンプルなリテラルやElixir実行結果だけを返す簡易APIまでの実装だったので、これをREST API化したいと思います

今回は、まずGET、つまり「参照系REST API」を実装します

# 本コラムの検証環境

本コラムは、以下環境で検証しています（Windowsで実施していますが、Linuxやmacでも動作する想定です）

- Windows 10
- Elixir 1.10.1 　　　　 ※[<b>最新版のインストール手順はコチラ</b>](https://qiita.com/piacere_ex/items/6714e1440e3f25fb46a1#%E4%BA%8B%E5%89%8D%E6%BA%96%E5%82%99elixir%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
- Phoenix 1.4.15　　　※[<b>最新版のインストール手順はコチラ</b>](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)
- Node.js 12.14.0

なおPhoenixは、1.3系でも動作します（ElixirもPhoenixのバージョンに準じた古いものでも大丈夫です）

あと下記コラムの続きとして実施するので、未実施であれば、事前に実施しておいてください（近々、ボイラープレート生成のmixコマンドとしてOSS化は考えています）

[<b>PHP的ハックを応用してNode.js Express／Go的な軽量APIをPhoenixで実現してみた</b>](https://qiita.com/piacerex/items/144089aa6f2052f4d1cb)
|> [<b>Express／Go的なPhoenix軽量APIの上に参照系REST APIを実装する</b>](https://qiita.com/piacerex/items/e11ef7b5cdcc81ac69c9)

# 手順①：数値のハンドリングにsmallex導入

REST APIは、URLでid指定しますが、ここの判定をスマートにするために、[smallexの型判定モジュール](https://hexdocs.pm/smallex/Type.html)を使いたいので、[smallex](https://hex.pm/packages/smallex)をインストールします

```elixir:mix.exs
defmodule Basic.MixProject do
…
  defp deps do
    [
      { :smallex, "~> 0.0" },
      …
```

一度、iexを抜けて、下記でsmallexをインストールします

```
mix deps.get
iex -S mix phx.server
```

# 手順②：REST APIをルーティング不要に

前回同様、ルーティングやMVCの追加無しに、REST APIを増設できるようにします

REST APIのエントリーポイントは、「/api」配下に集約し、下記の汎用ルーティング4つを追加します

```elixir:/lib/basic_web/router.ex
defmodule BasicWeb.Router do
…
  scope "/api/", BasicWeb do
    pipe_through :browser

    # v-- add start
    get    "/*path_", ApiController, :index
    post   "/*path_", ApiController, :create
    put    "/*path_", ApiController, :update
    delete "/*path_", ApiController, :delete
    # ^-- add end
  end
…
```

なお、mix phx.gen.jsonで自走生成される元々のREST APIであれば、「resources」を使い、4つでは無く、1つで済ませますが、「resources」は、下記の通り、ワイルドカード指定がNGっぽいです（ここの原因究明は別の機会で）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/bff9b00c-7110-6f5e-edd5-1daa884f8a7f.png)

# 手順③：indexのAPIコントローラ実装不要に

GETのURL末尾が、数値で無いときはindex.jsonを、数値のときはshow.jsonを返すようにします

タプルでのパターンマッチによる束縛と、if式のタプル返却が、Elixirらしいコンパクトで、差分が分かりやすいコードです

```elixir:lib/basic_web/controllers/api_controller.ex
defmodule BasicWeb.ApiController do
  use BasicWeb, :controller

  def index( conn, params ) do
    id = params[ "path_" ] |> List.last |> Type.to_number
    { new_params, template, path } = 
      if id == nil do
        { 
          params, 
          "index.json", 
          params[ "path_" ]
        }
      else
        { 
          params |> Map.put( "id", id ), 
          "show.json", 
          params[ "path_" ] |> Enum.drop( -1 )
        }
      end
    prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/" 
    render( conn, "#{ prefix }#{ template }",  params: new_params )
  end
end
```

# 手順⑤：json.eexを追加する

## １）show.json.eexの追加

templates配下に、apiフォルダを掘り、その下にv1フォルダ（APIのバージョン指定用）を掘り、更にusersフォルダを掘ります

そして、users配下に、下記内容でshow.json.eex JSONテンプレートを作ります

ここの記述は、Elixirそのままのコードが使え、paramsでGETパラメータを参照可能です（これは上記コントローラ側でバインディングしています）

これにより、ageはGETパラメータで変わる仕様になっています

また、idの値は、URLに連動して変わるようにしています（これも上記コントローラ側でバインディングしています）

```elixir:lib/basic_web/templates/api/v1/users/show.json.eex
%{
  id:   params[ "id" ], 
  name: "Mr.xxx"
  age:  params[ "age" ]
}
```

RESTクライアントで``「GET http://localhost:4000/api/v1/users/123」``のアクセスを行うと、以下のような結果が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5b047b57-ccd3-c047-3948-9cc2e37fd9e6.png)

これで、バージョン付きAPIでの、id指定による1件JSON返却ができるようになりました

## ２）index.json.eexの追加

今度は、users配下に、下記内容でindex.json.eex JSONテンプレートを作ります

ここの記述も、Elixirそのままのコードが使えます

```elixir:lib/basic_web/templates/api/v1/users/index.json.eex
[
  %{
    id:   params[ 1 ], 
    name: "Mr.xxx"
  }, 
  %{
    id:   params[ 2 ], 
    name: "Ms.yyy"
  }, 
  %{
    id:   params[ 3 ], 
    name: "Mrs.zzz"
  }
]
```

RESTクライアントで``「http://localhost:4000/api/v1/users」``にアクセスすると、以下のような結果が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a3c68775-7397-3267-59ab-ab3243b933d3.png)

一応、バージョン付きAPIでの、一覧っぽいJSON返却はできるようになりましたが、show.json.eexと無関係なJSON定義になってしまっています…

## ３）show.json.eexを繰り返すindex.json.eexに修正

index.json.eexを、show.json.eexを繰り返すようなJSONテンプレートに修正します

JSONテンプレート内で、Elixirコードがそのまま使える特性を活かします

File.read!()で、show.json.eexを読み込み、for式の中で、Code.eval_string()にてshow.json.eexのparams指定してのテンプレート展開を繰り返すようにします

idは、for式のループカウンタ0～3を当てはめます

ageは、GETパラメータで指定したものが、全件に適用されます

```elixir:lib/basic_web/templates/api/v1/users/index.json.eex
json = File.read!( "lib/basic_web/templates/api/v1/users/show.json.eex" ) 

for i <- 0..3 do
  new_params = params |> Map.put( "id", i ) 
  json |> Code.eval_string( [ params: new_params ] ) |> elem( 0 )
end
```

RESTクライアントで``「http://localhost:4000/api/v1/users」``にアクセスすると、以下のような結果が表示されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/1cc2151e-5263-a4bd-fe37-f46977d38ad7.png)

これで、1件ずつJSONを反映した、一覧JSON返却ができるようになりました

下記のように、show.json.eex側を変更すると、index.json.eexも返却が変わることも確認します

```elixir:lib/basic_web/templates/api/v1/users/show.json.eex
%{
  id:   params[ "id" ], 
  no:   params[ "id" ] + 1, 
  age:  params[ "age" ]
}
```

RESTクライアントで``「http://localhost:4000/api/v1/users」``にアクセスすると、以下のように、show.json.eexの変更により、index.json.eexが変わることが確認できます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/a6821087-7dae-11ee-2c52-de9f4f48b234.png)

# 終わり

JSONテンプレート追加だけで実現できる、参照系REST APIを実装しました

まだDB等に接続していないため、変数展開は、ループカウントとGETパラメータくらいしか使えていませんが、[次回は、DBから値取得をする](https://qiita.com/piacerex/items/437e7b6c26c95dae0efb)ようにします

# p.s.このコラムが、面白かったり、役に立ったら…

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/67568d54-6584-ad33-77c7-b3b3afe70287.png) や ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/f5b819ef-d655-ef72-294c-b1a9d2c5b563.png) にて、どうぞ応援よろしくお願いします:bow: