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

  • 25
    いいね
  • 0
    コメント

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