Edited at
FOLIODay 7

BFFの設計とflowtype

More than 1 year has passed since last update.

株式会社FOLIOのフロントエンドエンジニアをしています、諸見里です。

この記事はFOLIOアドベントカレンダー7日目の記事です。昨日の記事はバズらせてサーバーを落とすことで有名な弊社CDOの超心に刺さってイヤでも印象に残るプレゼンの方法でした。それほどバズらずサーバーは落ちなかったようです。

会社のアドベントカレンダーということで、今回はFOLIOのBFF(Backend For Frontend)でのflowtypeの活用と、アーキテクチャについて書きます。


BFF & Microservices

FOLIOのシステムは、Scalaを中心としたマイクロサービスで構成されています。そして、それらのサービスをクライアント向けに束ねるために、Node.jsで書かれたBFF(Backend For Frontend)を置いています。例えばWebアプリ用のBFFなら、各サービスからデータを集めてきて、React.jsでのレンダリングまでを担当しています。

そして、現在開発中のモバイルアプリ向けにも、Node.jsで書かれたBFFをAPIサーバーとして置いています。今回はこのモバイル用BFFについての話です。


Flowtype

近年のモダンJavaScriptの流れにのり、FOLIOではフロントエンドとBFFでFlowtypeを使って型を意識した開発をしています。特にモバイル用BFFでは型のカバレッジがほぼ100%になるように意識をしています。そのために活用しているのが、各サービスとの通信に使っているRPCフレームワークthriftのIDL(インタフェース定義)と、BFF自体のAPIを定義したSwaggerです。


thrift IDL

各マイクロサービスのインタフェースはthrift IDLで定義されていて、この定義ファイルを元にthrift2flowを使用して型定義ファイルを生成します。これにより、各サービスからのレスポンスの型を保証することができるようになります。


Swagger

モバイル用BFFでは、APIの定義をSwaggerで記述しています。SwaggerはAPIに定義をyaml(JSON)でかけるもので、ドキュメント化してくれたりダミーレスポンスを返すサーバーを生成してくれます。

以下のようにAPI定義を書いていくと、HTMLでドキュメント化してくれます。

/user:

get:
description: ユーザー情報を取得する
responses:
200:
schema:
$ref: '#/definitions/User'
definitions:
User:
properties:
id:
type: string
name:
type: string

Swaggerを使うことでBFF開発者とアプリ開発者が認識を共有できるのはもちろん、アプリで使うModelファイルを自動生成できるようにもなっています。詳しくはSwaggerで始めるAPI定義管理とコードジェネレートと、Droid Kaigi 2018で予定されているTakuma Fujitaのセッションをご覧ください。

そしてBFFでも、Swaggerで書かれたAPI定義からswagger-to-flowtypeを使用して、APIパラメータ・レスポンスの型定義を生成しています。

BFFの役割は、各サービスからデータを集め、クライアント向けに整形して返すことです。つまり、各サービスから返ってくるデータの型と、クライアントに返すべき型が定まっていれば、理論的には100%型を保持した実装ができるようになります。


型をどこでつけるか

理論的には100%型を活用できる、と言ってもそれをどう実装に落としこむのかということが重要になります。というのも、例えばexpress.jsでは、通常Controllerは以下のように記述します。

app.get('/', function (req, res) {

const data = { msg: 'Hello World!' };
res.send(data);
});

フレームワークの都合上、Controller関数自体は値を返しません。res.send()の型定義もsend(obj: Object)のようになっていたりするので、Controllerのレイヤで型を縛るのは難しくなっています。

同様に、各サービスにリクエストを送る時もどのレイヤで型を縛るべきかが問題になります。(ここはREST APIの例ですが)、サービスにリクエストを送ってもそのレスポンスはanyとなります。

const request = require('request');

const { promisify } = require('utils');

// 何かしらの処理

// ここで型をつける?
const { body } = await promisify(request.get)({ url: 'http://hoge-service.example.com/' });

// 何かしらの処理

これらの問題をどうにかするために、MVC(まあBFFなんでViewはないんですが)ではなく、クリーンアーキテクチャに近いようなアーキテクチャを取り入れます。


クリーンアーキテクチャをベースに型を考える

DDD + Clean Architecture + UCDOM Full版の資料を参考に、


  • Controller(リクエストパラメータの処理&エラーレスポンスの処理)

  • UseCase(各サービスから必要なデータを集める)

  • Gateway(サービスと通信する)

  • Presenter(レスポンスを構築する)

の4層(+ビジネスロジックを持つDomain)に整理しました。そしてGatewayで各サービスのレスポンスに型をつけ、Presenter層でアプリに返すべきレスポンスの型を保証します。

それでは簡単なサンプルコードを。


Gateway

Gatewayでは、サービスからデータを取得して、型をつけるだけの責務です。ここではデータの加工は行わず、外部接続の隠蔽と型をつけることだけを目的にしています(実際のコードではこのレイヤをDIで入れ替えられるようにしています)。


HogeGateway.js

// 自動生成された型定義ファイルを読み込む

import type { Hoge } from '../types'

// 型をここで指定
const getHoge = (): Hoge => {
const { body } = await promisify(request.get)({ url: 'http://hoge-service.example.com/' });
// anyなObjectに型をつけて返すだけ
return body
}



Presenter

Presenterも同様にビジネスロジックは持たず、型に合わせてレスポンスデータを整形するだけです。


js+HogePresenter.js

// swaggerから生成された型定義ファイルを読み込む

import type { HogeResponse } from '../swagger/type'

// この関数でレスポンスの型が保証される
const responseHoge = (data): HogeResponse => {
return { msg: data.message };
}



UseCase

UseCaseはGateway(各種サービス)からデータを集め、必要な演算やデータの整形を行います。


HogeUseCase.js


class HogeUseCase {
getHoge() {
// Gatewayを通すと型付きのデータが得られる
const data = await HogeGateway.getHoge()
// 他にもいろいろなサービスからデータを集める

// いろいろな処理(実際はDomain層に移譲)

return data
}
}



Controller

そしてControllerはUseCaseの結果をpresenterに渡します。Presenterの命名規則をAPIのURLと合わせたりしておくと、Controllerが正しい(型の)レスポンスを返していることが一目瞭然になります。


HogeController

class HogeController {

hoge(req, res) {
try {
res.send(presenter.responseHoge(await HogeUseCase.getHoge()))
} catch (e) {
// エラーレスポンスはここで処理
}
}
}

こうして、Presenter/Gatewayのレイヤを必ず通すことで、型の恩恵を100%受けられるアーキテクチャになりました。


型を意識してアーキテクチャを考える

厳密なクリーンアーキテクチャではない、ライトなものですが、目的である「flowtypeの型を活かす」ことは達成できました。

Node.js(JavaScript)で使われるflowtypeやTypeScriptは、やはり「後付け」である以上、その型をどう使うかを考えて設計しないと、その恩恵をなかなか受けられないままとなってしまいます。

ただ実際にやっていることは、外部リソースへの接続部分を集約したり、モジュール(npm)を使うときはラップして必要なI/Fだけを公開(して型をつける)したり、純粋関数を意識するといったことで、結局のところ保守性の高い設計と言われているものに繋がるんだなと思います。

型の話以外にも、BFFは


  • 依存先が多くFixtureを作る難易度が高いので、テストできる/できないの境界線をどのレイヤに引くか


  • BFFはプラットフォームごとに個別に立てると言われているなかで、どのレイヤがモジュールとして共通化できるのか

  • 各サービスが(もしくはサービスのAPI単位で)ダウンしたとき、BFFはfail softに倒すのか、エラーにするのか、どちらにも倒せるエラーハンドリングの設計をする

など、品質について考慮すべきことも多いです。堅牢で高品質なBFF・フロントエンド開発に興味がある方をFOLIOでは募集しています!(もちろん他の職種も)

https://www.wantedly.com/companies/folio/projects


明日の記事は、証券分析や運用アルゴリズム担当による「バリュー投資とかグロース投資とかの整理(仮)」です。今後もバラエティ豊かなネタが続くので、気になる方は購読を!