13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2020-04-06

【本コラムは、本題は1分で読めて、詳細は10分くらいで読めて、10分くらいでお試しいただけます】
piacere です、ご覧いただいてありがとございます :bow:
image.png

軽量REST APIを使った、BFF(Backend for Frontend)的APIの例として、Scaffoldした2テーブルをJOINするAPIを作ってみます

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

「40秒で支度しな」
image.png

ということです:yum:

なお「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でも動作する想定です)

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

手順①【0秒~36秒】:JSONテンプレートを書く

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

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

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

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

たったこれだけで、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

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

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 ..

付録【事前準備C】軽量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

付録【事前準備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本のルーティングを追加します

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

付録【事前準備E】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

下記データを、ブラウザで「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

終わり

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

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

次回は、JSONテンプレート内でAPIコールを書くことで、今度は本物のBFFを、やはり40秒で構築してみます

13
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?