Edited at

Node.js + GraphQLでBFFを作った話

More than 1 year has passed since last update.


whoami



  • @qsona (Twitter, GitHub, Qiita)

  • Node.js developer

  • FiNC 2016/2 〜


    • Ruby, Rails / MySQL

    • love microservices!



  • Microservices Meetup 主催





BFF とは?


  • Backends for Frontends の略

  • クライアントとバックエンドの中間にサーバを置き、フロントエンド寄りの処理を行う

  • Microservicesの文脈で語られることが多い

  • 昨日の会長のスライド step by step BFF



GraphQL とは?


  • クエリー型 Web API


    • RESTful API において問題になりがちな点をカバーしている



  • 仕様として定められている


    • (RESTfulはあくまでAPI設計の指針)





Node.js + GraphQL でBFFを作った


  • Node.js v6.x


    • v7で --harmony オプションで async-await...!

      は止められましたw

    • ※ v7.6でオプションなしでも使えるようになった



  • koa v2.x



構成図

Android    iOS   WebApp

| | | GraphQL API
Backends-for-Frontends (Node.js)
| | | RESTful API
Backend1 Backend2 Backend3
Rails Rails Node.js

※ 現状は、全てのリクエストでBFFを通しているわけではない



気をつけたこと


  • 作り込み過ぎず、早めに試せることを重視した


    • 価値検証を重視、ある程度失敗を許容する



  • 役割をいきなり一手に引き受けすぎない


    • BFFがドメインロジックを持つようになると悲劇





BFFの役割として選択したもの


  • 複数のAPI呼び出しをまとめる (batch request)

  • APIを順番に呼び出し、結果を合成する


    • 例: ランキング

    • ランキングのサービスはuser_idを含む配列を返す

    • ユーザ情報を別のサービスにuser_idsで引く



  • ユーザ認証


    • Backendへのリクエストは user_id + サービスとしての認証トークン

    • user_idはHTTPヘッダに入れた





BFFの役割として選択しなかったもの


  • クライアント向けたレスポンスの整形

  • バックエンドへのqueryingの仕方を持つ


    • 例: 一覧画面でのフィルターやソートのかけ方



  • 更新系をまとめる処理

BFFにドメインロジックが入る可能性を排除したかった



Why Node.js?


  • (我々の)BFFのメインの役割は、複数のBackend APIを呼び出して返すこと


    • 当然、並列に呼び出したい。非同期処理が大半になる



  • フロントエンジニアに触りやすくしたかった


    • 我々のクライアントサイドの言語: Swift, Java (Kotlin), JavaScript

    • (そもそもSwiftやJavaはここの選択肢に入りにくい)

    • JavaScriptはとっつきやすい(※個人の感想です)

    • 重厚な言語(※個人の感想です)は選択肢に入れにくい





Why Node.js?


  • qsonaがもともとNode畑だったので


    • 手伝ってくれた人もJavaScript畑



  • ReactのSSRを見越す



Why GraphQL?



GraphQLの特徴 (1)

複数のリソースを同時に取れる

query {

user(id: 10) {
name
age
}
groups(filter: 'awesome') {
name
leader_name
}
}

{

"data": {
"user": { "name": "qsona", "age": 17 },
"groups": [
{ "name": "Node学園", "leader_name": "yosuke_furukawa" },
{ "name": "FiNC", "leader_name": "Yuji Mizoguchi" }
]
}
}

今回は batch requestと呼びます。



GraphQLの特徴 (2)

あるリソースのうち、必要なkeyのみを取得できる

query {

user(id: 10) {
name
// ageがない
}
}

{

"data": {
"user": { "name": "qsona" }
}
}

今回はfiltering queryと呼びます。



GraphQLの特徴 (3)

ネストされたリソースの、どの階層まで取るかを決められる

query {

user(id: 10) {
name
age
children {
name
age
}
}
}

{

"data": {
"user": {
"name": "qsona",
"age": 17,
"children": [
{ "name": "junki", "age": 4 },
{ "name": "sumire", "age": 2 }
]
}
}
}

今回は preload query と呼びます。



GraphQLの特徴 (4)

強力な型システムをもつ (GraphQLType)

それによって得られるもの


  • リクエストのバリデーション

  • レスポンスのバリデーション

  • ドキュメントの自動生成

  • cli (GraphiQL) の自動生成


    • とても使いやすい





BFF導入前にあった問題点 (1)

Smart UIなAPI


  • 画面に必要なものを全て返すようなAPI

  • 画面が変わるたびに、1つのレスポンスにキーが追加される


    • キーが増えすぎる

    • 何が今でも使われているのか不透明



  • メンテが大変



問題点 (1) への対応


  • 前提: Backendをリソース指向に寄せていく

  • それでも画面に必要なものを同時に取得できるように


    • batch request

    • preload query





BFF導入前にあった問題点 (2)

レスポンスのjsonの形が不安定 => クライアントが困る


  • レスポンスのドキュメントがない


    • DSLでリクエストパラメータを記述していた (grape)

    • ここからドキュメント(swagger)を自動生成できる

    • しかし、レスポンスのドキュメントはない



  • リソースが定まってない


    • ふわっとしたJSONシリアライザ ( rabl )

    • 複数のAPI間で、jsonの形が微妙に違う

    • Android/iOS間でのドメインの解釈も定まっていなかった





問題点 (2) への対応


  • 前提: Backendをリソース指向に寄せていく

  • GraphQLType



BFF導入前にあった問題点 (3)


  • メインのサービスが、他のサービスのデータもまとめて返していた


    • オーケストレーション



  • 凝集度の低下


    • 他サービスの変更時、メインのサービスまで変更しなければならない

    • (メインのサービスは規模が大きく、変更・デプロイが大変)





問題点 (3) への対応


  • 前提: Backendはリソース指向に寄せていく

  • batch requestを利用


    • 複数APIの集約はBFFの責務とする




(画像)



改善した :muscle:



その後



BFF導入後の問題点 (1) クエリの管理

GraphQLの特徴 (2) で紹介した "filtering query" は特に何も解決していない


  • あるリソースのうちの一部分だけ取得したい、というユースケースがほとんどなかった


    • クライアントが、部分的なエンティティをわざわざ作らなければならない

    • 部分で取得すると、キャッシュもヒットできなくなる





BFF導入後の問題点 (1) クエリの管理


  • バックエンドにキーを追加するたびに、変更する必要がある

  • iOSとAndroidでの共通化


    • 本来は「iOSとAndroidで別々な方法でリソースを取れる」というメリットがある

    • しかし、そのようなユースケースは今の所なかった

    • クエリを共通管理することに





BFF導入後の問題点 (2) 改善の競合


  • Backendサーバ (Rails) 側も問題を改善してきた


    • JSON Hyper Schemaの導入

    • 使うシリアライザの変更 (rabl => active_model_serializers)

    • API設計レビュー、設計力の向上、設計の標準化



  • クライアントも、ドメインモデルの手前にJSONをマップする層を持っている





BFF導入後の問題点 (2) 改善の競合


  • 役割が結構かぶる


    • Server: JSON Schema / Serializer

    • BFF: GraphQLType

    • Client: 腐敗防止層



  • 必要かどうかは、チームの距離感の問題でもある


    • 結合度

    • そこまで遠くする必要はない





落穂拾い



落穂拾い (1) 更新系はどうしている?


  • ひとまず、更新系はBFFに持っていない

  • 更新系のオーケストレーションは、今後もしない

  • やるとしたら「このボタン(アクション)に対応するエンドポイントをBFFで持つ」


    • GraphQLではちょっと難しい





落穂拾い (2) エラー処理


  • GraphQLでは、レスポンスは errors または data のキーを返す

  • 複数リソースを同時に取得しようとして、1つが失敗した場合、errorsだけ返すのが仕様

  • BFFでは、成功したリソースは返しつつ、失敗したリソースはその理由を返す必要がある


    • つまり errors は基本使えない





落穂拾い (2) エラー処理

{

"data": {
"user": {
"isError": false,
"data": {
"name": "qsona"
}
},
"groups": {
"isError": true,
"reason": "Server is broken"
}
}
}



落穂拾い (3)


  • GraphQLInt型は32bitまでしか対応していない

  • オーバーフローして障害が発生 :cry:



振り返り


  • GraphQLをBFFに採用するのは、少なくとも銀の弾丸ではない


    • 資産が使えるので、やりたいことにマッチすればメリットがある

    • 自由度が効きにくい面もある





振り返り

ハイブリッド策を取るとよかったかも


  • BFF置くならやはり全部のリクエストを通したい。

  • 初めはただのProxyでいい。 (step by step BFF 参照)

  • GraphQLは、1つのエンドポイント (例えば POST /v1/graphql) を提供するだけ


    • 同じサーバで他のエンドポイントを提供しても問題ない



  • エンドポイントは画面ごとに提供して、BFF内でGraphQLを完結させることもできる


    • (それをやりたいかはともかく...)





Thank you!

We are hiring!!


  • Microservices


    • hot: Asynchronus architecture



  • Node.js Server

  • React, Redux

  • GraphDB (TitanDB)

  • Fulltext Search

  • Ruby on Rails


    • v5 / api mode



  • Docker, Amazon ECS

  • iOS Swift / Android Java, Kotlin

  • Machine Learning