【本コラムは、本題は1分で読めて、詳細は10分くらいで読めて、10分くらいでお試しいただけます】
piacere です、ご覧いただいてありがとございます
軽量REST APIを使った、BFF(Backend for Frontend)的APIの例として、Scaffoldした2テーブルをJOINするAPIを作ってみます
軽量APIでのBFF的API構築は、JSONテンプレートを1本作るだけなので、あまりに短時間で済むため、
ということです
なお「BFF」は、「Backend for Frontend」の略で、フロントエンド毎でプレーンAPI呼出を前提としてロジックを書くのを止め、APIファサードでビジネスロジックを共通提供する考え方で、これにより、フロントエンド毎にビジネスロジックを開発するムダを省きます
BFF(Backends For Frontends)超入門 ― Netflix、Twitter、リクルートテクノロジーズが採用する理由
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 ※最新版のインストール手順はコチラ
- Phoenix 1.4.15 ※最新版のインストール手順はコチラ
- Node.js 12.14.0
本内容の実施前提として、本コラム中盤以降の「事前準備A~E」は終えておいてください
手順①【0秒~36秒】:JSONテンプレートを書く
後半の「事前準備A~E」を済ませた後、ここからが40秒間の本番です
上記で投入したDBデータをJOIN取得し、上記JSONテンプレートに適用する一覧JSONテンプレートを作ります
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返却されます
「これだけで本当に完成です」
たったこれだけで、BFF的API構築が完了します
また、このBFFは、JSONテンプレートをいじるだけで、カスタマイズやメンテナンスが可能です
APIバージョンアップやエントリーポイント変更も、フォルダ移動するだけです、とても簡単です
別のBFFを生やしたいときも、フォルダ掘って、JSONテンプレート置くだけで、ルーティング/MVCを書く必要はありません
そして、このPhoenix PJ1本を使い回したり、フォルダ毎コピーするだけで、すぐにBFFやAPIが構築できます
PhoenixのScaffoldによるAPI生成も、ただ生成するだけなら簡単ですが、自動生成されたコードをキャッチアップしたり、上記したようなカスタマイズを行うのは骨が折れるので、JSONテンプレートをいじるだけでAPI構築/メンテナンスできるPhoenix軽量APIは、けっこう便利に使えると思います
付録【事前準備A】PhoenixからPostgreSQLを利用可能に
(※環境構築済の方は実施不要です)
ローカルでPostgreSQLを使えるようにしたいので、下記を実施します
【PostgreSQL or MySQL編】Excelから関数型言語マスター3回目:WebにDBデータ表示
https://qiita.com/piacerex/items/b6d67d4fd628565979e4
付録【事前準備B】PJ作成、ライブラリ導入、DB作成
Phoenix PJを作成します
mix phx.new bff
Fetch and install dependencies? [Yn] n
cd bff
必要ライブラリを追加します
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 ..
付録【事前準備C】軽量APIフレームワークを配置
前回までに作成した、以下の軽量APIフレームワークコード群を、上記で作成したPhoenix PJ内に置き、「/api」配下に軽量APIのエントリーポイントを置きます(ただし各種モジュール名は今回PJに合わせて変更しています)
そんなに遠くないタイミングに、mixコマンド一発で終わるようOSSライブラリ化しようと思っています
なおDbモジュール/DbMnesiaモジュールは、今回、Mnesiaを使わないため、持ってきません
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
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
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
…
付録【事前準備D】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本のルーティングを追加します
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
付録【事前準備E】Web CRUDからテストデータ投入
下記データを、ブラウザで「http://localhost:4000/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
下記データを、ブラウザで「http://localhost:4000/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
終わり
Phoenix軽量APIを使い、JSONテンプレート追加だけで、BFF的APIを構築できました
40秒くらいで、本当に構築できるということが実感いただけたら幸いです