「BFF的APIを40秒で支度しな」：Phoenix軽量APIでScaffoldした2テーブルをJOIN

【本コラムは、本題は1分で読めて、詳細は10分くらいで読めて、10分くらいでお試しいただけます】
[piacere](https://twitter.com/piacere_ex) です、ご覧いただいてありがとございます :bow:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/086dca63-bd51-531c-7c27-d0004acb4633.png)

[軽量REST API](https://qiita.com/piacerex/items/78d3036e556e66981c79)を使った、[BFF（Backend for Frontend）](https://www.atmarkit.co.jp/ait/articles/1803/12/news012.html)的APIの例として、Scaffoldした2テーブルをJOINするAPIを作ってみます

軽量APIでのBFF的API構築は、JSONテンプレートを1本作るだけなので、あまりに短時間で済むため、

<b>「40秒で支度しな」</b>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/54c2b51d-e52b-b1c5-dc3a-e4ef63d0cef5.png)

ということです:yum:

なお「BFF」は、「Backend for Frontend」の略で、フロントエンド毎でプレーンAPI呼出を前提としてロジックを書くのを止め、APIファサードでビジネスロジックを共通提供する考え方で、これにより、フロントエンド毎にビジネスロジックを開発するムダを省きます

[<b>BFF（Backends For Frontends）超入門 ― Netflix、Twitter、リクルートテクノロジーズが採用する理由</b>](https://www.atmarkit.co.jp/ait/articles/1803/12/news012.html)
https://www.atmarkit.co.jp/ait/articles/1803/12/news012.html

※事前準備としての、「PhoenixでPostgreSQL利用可能にする」「2テーブルのScaffold」「軽量APIフレームワークのコピペ」「テストデータ投入」は、40秒には含みません

# 本コラムの検証環境

本コラムは、以下環境で検証しています（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

本内容の実施前提として、本コラム中盤以降の「事前準備Ａ～Ｅ」は終えておいてください

# 手順①【0秒～36秒】：JSONテンプレートを書く

後半の「事前準備Ａ～Ｅ」を済ませた後、ここからが40秒間の本番です

上記で投入したDBデータをJOIN取得し、上記JSONテンプレートに適用する一覧JSONテンプレートを作ります

```elixir:lib/bff_web/templates/api/v1/team_members/index.json.eex
import Ecto.Query
datas = Bff.Repo.all( 
	from u in Bff.Api.User, 
	join: t in Bff.Api.Team, on: u.team_id == t.id, 
	select: %{ "name" => u.name, "age" => u.age, "team" => t.name, "url" => t.url } )
for data <- datas do
	%{
		name: data[ "name" ], 
		age:  data[ "age" ], 
		team: data[ "team" ], 
		url:  data[ "url" ], 
	}
end
```

# 手順②【36秒～40秒】：動作確認

RESTクライアントで``「GET http://localhost:4000/api/v1/team_members」``のアクセスを行うと、以下のように、JOIN済みデータ一覧が、JSON返却されます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/5c0cf51a-cec5-5fa9-d787-d1369a59fcea.png)

# 「これだけで本当に完成です」

たったこれだけで、BFF的API構築が完了します

また、このBFFは、JSONテンプレートをいじるだけで、カスタマイズやメンテナンスが可能です

APIバージョンアップやエントリーポイント変更も、フォルダ移動するだけです、とても簡単です

別のBFFを生やしたいときも、フォルダ掘って、JSONテンプレート置くだけで、ルーティング／MVCを書く必要はありません

そして、このPhoenix PJ1本を使い回したり、フォルダ毎コピーするだけで、すぐにBFFやAPIが構築できます

PhoenixのScaffoldによるAPI生成も、ただ生成するだけなら簡単ですが、自動生成されたコードをキャッチアップしたり、上記したようなカスタマイズを行うのは骨が折れるので、JSONテンプレートをいじるだけでAPI構築／メンテナンスできるPhoenix軽量APIは、けっこう便利に使えると思います

# 付録【事前準備Ａ】PhoenixからPostgreSQLを利用可能に

（※環境構築済の方は実施不要です）

ローカルでPostgreSQLを使えるようにしたいので、下記を実施します

[<b>【PostgreSQL or MySQL編】Excelから関数型言語マスター3回目：WebにDBデータ表示</b>](https://qiita.com/piacerex/items/b6d67d4fd628565979e4)
https://qiita.com/piacerex/items/b6d67d4fd628565979e4

# 付録【事前準備Ｂ】PJ作成、ライブラリ導入、DB作成

Phoenix PJを作成します

```
mix phx.new bff
Fetch and install dependencies? [Yn] n
cd bff
```

必要ライブラリを追加します

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

ライブラリのインストール、DB作成を行います

```
mix deps.get
mix ecto.create
```

アセットをビルドします

```
cd asset
npm install
cd ..
```

# 付録【事前準備Ｃ】軽量APIフレームワークを配置

前回までに作成した、以下の軽量APIフレームワークコード群を、上記で作成したPhoenix PJ内に置き、「/api」配下に軽量APIのエントリーポイントを置きます（ただし各種モジュール名は今回PJに合わせて変更しています）

そんなに遠くないタイミングに、mixコマンド一発で終わるようOSSライブラリ化しようと思っています

なおDbモジュール／DbMnesiaモジュールは、今回、Mnesiaを使わないため、持ってきません

```lib/bff_web/controllers/api_controller.ex
defmodule BffWeb.ApiController do
  use BffWeb, :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

  def create( conn, params ) do
    path = params[ "path_" ]
    prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/" 

    # TODO：data.json.eexと列突合チェック
    result = execute( "#{ prefix }create.json", params: params )

    if elem( result, 0 ) == :ok do
      new_params = params |> Map.put( "id", elem( result, 1 ) )
      conn
      |> put_status( :created )
      |> render( "#{ prefix }show.json", params: new_params )
    else
      result
    end
  end

  def update( _conn, _params ) do
    id = params[ "path_" ] |> List.last |> Type.to_number
    new_params = params |> Map.put( "id", id )
    path = params[ "path_" ] |> Enum.drop( -1 )
    prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/" 

    # TODO：data.json.eexと列突合チェック
    result = execute( "#{ prefix }update.json", params: new_params )

    if elem( result, 0 ) == :ok do
      conn
      |> put_status( :created )
      |> render( "#{ prefix }show.json", params: new_params )
    else
      result
    end
  end

  def delete( conn, params ) do
    id = params[ "path_" ] |> List.last |> Type.to_number
    new_params = params |> Map.put( "id", id )
    path = params[ "path_" ] |> Enum.drop( -1 )
    prefix = if params[ "path_" ] == nil, do: "", else: Enum.join( path, "/" ) <> "/" 

    # TODO：data.json.eexと列突合チェック
    result = execute( "#{ prefix }delete.json", params: new_params )

    if elem( result, 0 ) == :ok do
      conn
      |> put_resp_header( "content-type", "application/json; charset=utf-8" )
      |> send_resp( :no_content, "" )
    else
      result
    end
  end

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

```lib/bff_web/views/api_view.ex
defmodule BffWeb.ApiView do
  use BffWeb, :view

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

```lib/bff_web/router.ex
defmodule BffWeb.Router do
…
  scope "/api/", BffWeb do
    pipe_through :browser

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

# 付録【事前準備Ｄ】2テーブル分のWeb CRUDをScaffold

Phoenix標準機能のScaffold（mix phx.gen.html）を使って、2テーブル分のWeb CRUDを自動生成します

```
mix phx.gen.html Api User users name:string age:integer team_id:integer
mix phx.gen.html Api Team teams name:string url:string
Would you like to proceed? [Yn] Y
```

Web CRUD2本のルーティングを追加します

```lib/bff_web/router.ex
defmodule BffWeb.Router do
…
  scope "/", BffWeb do
    …
    resources "/users", UserController  # <-- add here
    resources "/teams", TeamController  # <-- add here
  end
…
```

マイグレーションし、Phoenix起動します

```
mix ecto.migrate
iex -S mix phx.server
```

# 付録【事前準備Ｅ】Web CRUDからテストデータ投入

下記データを、ブラウザで``「http://localhost:4000/teams/」``を開き、作成します

```:teamsで投入するテストデータ
id: 1, name: カラビナテクノロジー株式会社, url: https://karabiner.tech
id: 2, name: 公立大学法人 北九州市立大学,  url: https://www.kitakyu-u.ac.jp
id: 3, name: 有限会社デライトシステムズ,   url: https://www.delightsystems.com
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/10f1e869-cd49-f17c-786a-e675c6dd5bd7.png)

下記データを、ブラウザで``「http://localhost:4000/users/」``を開き、作成します

```:usersで投入するテストデータ
id: 1, name: つちろー,   age: 36, team_id: 1
id: 2, name: enぺだーし, age: 51, tema_id: 3
id: 3, name: ざっきー,   age: 47, team_id: 2
id: 4, name: piacere,   age: 45, team_id: 1
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/155423/e225f2cb-a56e-aadc-337e-5d12379c12db.png)

# 終わり

Phoenix軽量APIを使い、JSONテンプレート追加だけで、BFF的APIを構築できました

40秒くらいで、本当に構築できるということが実感いただけたら幸いです

[次回は、JSONテンプレート内でAPIコールを書くことで、今度は本物のBFFを、やはり40秒で構築してみます](https://qiita.com/piacerex/items/cd8395e16588cb86ed99)