Phoenixお気軽API開発①：PHP的ハックを応用してNode.js Express／Go的な軽量APIをPhoenixで実現してみた

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

[前々作は「PHP的気軽さでWebアプリを作る方法」](https://qiita.com/piacerex/items/25c82153976dcb6d57c1)、[前作はこれを応用して「リアルタイムフロントであるLiveViewにも気軽さを導入」](https://qiita.com/piacerex/items/f6963b87a1bf624b506f)してみましたが、今作では、Phoenix API開発に、Node.js Express／Go的な軽量APIのテイストを付与してみます

この対応により、APIも、WebアプリやLiveView同様、ルーティング／MVCが不要になります

また、従来通りのmix phx.gen.jsonによるScaffold（≒コード自動生成）で構築されるAPIだと、JSONフォーマットがカスタマイズしにくかったり、カスタムフィールドを乗せるのにもノウハウが必要ですが、ここでは、シンプルなJSONテンプレートによるAPI構築を実現するので、普段のPhoenix使いでは体験できない気楽さを感じていただけたら幸いです

<h3>:ocean::ocean::ocean: Advent Calendar、fukuoka.ex1位、Elixir2位達成ヽ(=´▽`=)ﾉ :ocean::ocean::ocean:</h3>

fukuoka.ex Advent Calendar、Webテクノロジーカテゴリで堂々1位 … 各コラムぜひお読みください
https://qiita.com/advent-calendar/2020/fukuokaex
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/8a22a81f-2917-6072-10bd-525e0fc24b75.png)](https://qiita.com/advent-calendar/2020/neosvr_reso)

そして、プログラミング言語カテゴリは、1位がRust、2位がElixir、3位がGoとモダン言語揃い踏みでのトップ3、熱いネー:laughing:
https://qiita.com/advent-calendar/2020/elixir
[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/ea185203-c525-c778-dc10-6d2ecc391616.png)](https://qiita.com/advent-calendar/2020/elixir)

# 本コラムの検証環境

本コラムは、以下環境で検証しています（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のバージョンに準じた古いものでも大丈夫です）

# 事前準備：PHP的ハック導入済みPhoenix PJの作成

下記コラムの手順で、PHP的ハック導入済みのPhoenix PJを作成します

[<b>ElixirでもPHP的に気軽なWebアプリ開発をしたい（Phoenixより薄いWebFW実現？）</b>](https://qiita.com/piacerex/items/25c82153976dcb6d57c1)
https://qiita.com/piacerex/items/25c82153976dcb6d57c1

ここまでが終わったら、ブラウザで``「http://localhost:4000/abc/def?abc=xyz123」``にアクセスすると、以下のようなページが表示されることを確認して、準備完了です
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/aff62c93-8848-0a24-e09e-cc2662e3d6f4.png)

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

まずはルーティングで、前作同様、「*path_」というワイルドカード指定をすることで、多階層のURLパスを「path_」パラメータから取得できるようにします

接続先のコントローラは、API専用のマッピングコントローラとします

なお、前作の続きでやられている場合は、getやpostのワイルドカード指定とぶつかるため、getやpostの分は、コメントアウトや削除しておいてください

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

    get "/", PageController, :index
#    get "/*path_", PageController, :index  # <-- remove here
#    post "/*path_", PageController, :index  # <-- remove here
#    live "/*path_", LiveViewController  # <-- remove here
    get "/*path_", ApiController, :index  # <-- add here
…
```

# 手順②：html.leexの複数階層フォルダ指定可能に

ここは上記のコラム内で実施済みなので、特にやることはありません

# 手順③：パスの自動マッピング、GETパラメータの素通し

API専用のマッピングコントローラを追加します

「path_」パラメータのフォルダ階層リストを、json.eexのパスにマッピングします

また、mix phx.newで生成されたコントローラが「_params」という感じでアンダースコアで未使用にしているので、これを解除し、renderの第3引数に渡すことで、GETパラメータを素通しさせます

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

  def index( conn, params ) do
    template = if params[ "path_" ] == nil, do: "index.json", else: Path.join( params[ "path_" ] ) <> ".json"
    render( conn, template, params: params )
  end
end
```

「path_」パラメータ未指定時のためのindex.json.eexを追加します

なお、JSONテンプレートは、templates配下に「api」というフォルダを掘って、配置することにします

```erb:lib/basic_web/templates/api/index.json.eex
%{
}
```

# 手順④：json.eexをビュー向けにテンプレート展開

API専用のビューを追加します

PhoenixにおけるJSONレンダリングは、Phonenixのビュー配下で、マップもしくはマップリストをJSONに変換することで実現されるため、コントローラでのrender()、つまりeexテンプレート展開（<%= ～ %>）のように文字列展開されると、都合が悪いです

そこで、ビューのrender()にて、Code.eval_string()を使って、自前でテンプレート展開を行います

Elixirのビルトインモジュールや自作モジュールは、Code.eval_string()で展開可能ですが、入力パラメータは自前でバインディングをかけなければならないため、Code.eval_string()の第2引数にてバインディングを行います

ちなみに、Code.eval_file()というもっと便利そうなものもあるのですが、Code.eval_string()のようなバインディングができないため、今回の目的には沿いませんでした

Phoenixは、カスタムテンプレートエンジンを定義できるので、JSON専用テンプレートを構築することもできるのですが、今回は、バインディングもできる強力なCode.eval_string()の恩恵に預かることにしました

```elixir:lib/basic_web/views/api_view.ex
defmodule BasicWeb.ApiView do
  use BasicWeb, :view

  def render( path, %{ view_template: view_template, params: params } ) do
    File.read!( "lib/basic_web/templates/api/#{ view_template }.eex" ) 
    |> Code.eval_string( [ params: params ] ) 
    |> elem( 0 )
  end
end
```

なお、Webページテンプレート同様、「@ param」で入力パラメータ指定可能にしたかったのですが、Code.eval系はモジュールアトリビュートが通常変数と扱いが異なるためNGで、EEx.eval系は@が利用できますが文字列返却になってしまうため、断念しました

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

## １）templates/api直下にjson.eex追加

これで、json.eexを追加するだけで、新たなAPIが追加可能となったので、追加してみます

JSONテンプレートは、Elixirのマップとリストで構成し、中身は通常のマップやリストと同様、Elixirコードを記述できます

まずはシンプルに、マップ1つだけとし、入力パラメータ／リテラル／Elixirビルトインモジュール関数を値に設定します

```erb:lib/basic_web/templates/api/sample.json.eex
%{
  id:      params[ "id" ], 
  name:    "Elixir", 
  version: System.version()
}
```

RESTクライアントで``「GET http://localhost:4000/sample?id=123」``のアクセスを行うと、以下のような結果が表示されます

入力パラメータや、Elixirビルトインモジュール関数が実行され、その結果がJSONとして返却されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/878c6fab-aa13-47ce-13e6-d6e54662ad7c.png)

idを変えると、返却されるJSONも変わります
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/41235003-bbb1-6dcc-9be5-e7bf2ae8a5d5.png)

## ２）templates/api配下のサブフォルダ下にjson.eex追加

apiフォルダ配下にフォルダを掘って、その配下にjson.eexを配置しても動きます

「abc」というフォルダを掘り、「def.json.eex」というJSONテンプレートを追加します

今度は、リストマップで構成し、各マップの値をランダム値で変動させてみます

```erb:lib/basic_web/templates/api/abc/def.json.eex
[
  %{
    greeting:  "I'm def-1", 
    type:      "plane API", 
    random_no: :rand.uniform
  }, 
  %{
    greeting: "I'm def-2", 
    type:     "plane API", 
    random_no: :rand.uniform
  }, 
  %{
    greeting: "I'm def-3", 
    type:     "plane API", 
    random_no: :rand.uniform
  }
]
```

RESTクライアントで``「GET http://localhost:4000/abc/def」``のアクセスを行うと、以下のような結果が表示されます

ランダム値が展開されたJSONオブジェクトを梱包するJSON配列が返却されました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5b2cf35e-49bb-775a-cd0f-f4ab04b4f047.png)

# 終わり

JSONテンプレート追加だけで実現できるお気軽API開発を実現してみました

[前々作のPHP的ハック](https://qiita.com/piacerex/items/25c82153976dcb6d57c1)をAPIに応用すると、Scaffoldを用いることなく、ルーティング／MVCにも煩わされない、軽量なAPI開発が可能となることがお分かりいただけたでしょうか？

Phoniexは、ファットなWebフレームワークだと誤解されがちですが、ここまでの3部作で見てきたように、ちょっとした工夫を施すことで、軽量フレームワークに化ける「プログレッシブWebフレームワーク」としての一面があり、そこが上手く共有できていたら嬉しいです

さて、[次回以降では、より本格的なAPI利用に向けて、REST API対応（POST／PUT／DELETEやURLとしてのid指定）や、軽量APIからのDB利用、APIバージョン管理とWebページ共存なども行っていきたい](https://qiita.com/piacerex/items/e11ef7b5cdcc81ac69c9)と思います

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