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

# whoami

- @qsona (Twitter, GitHub, Qiita)
- Node.js developer
- FiNC 2016/2 〜
  - Ruby, Rails / MySQL
  - love microservices!
- Microservices Meetup 主催
  - 昨日3/30に開催: [vol.5 (API Gateway & BFF)](https://togetter.com/li/1095919)

---

# BFF とは?

- Backends for Frontends の略
- クライアントとバックエンドの中間にサーバを置き、フロントエンド寄りの処理を行う
- Microservicesの文脈で語られることが多い
- 昨日の会長のスライド [step by step BFF](https://speakerdeck.com/yosuke_furukawa/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)

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

```graphql
query {
  user(id: 10) {
    name
    age
  }
  groups(filter: "awesome") {
    name
    leader_name
  }
}
```

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

今回は batch requestと呼びます。

---

# GraphQLの特徴 (2)

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

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

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

今回はfiltering queryと呼びます。

---

# GraphQLの特徴 (3)

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

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

```json
{
  "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](https://github.com/ruby-grape/grape))
  - ここからドキュメント(swagger)を自動生成できる
  - しかし、レスポンスのドキュメントはない
- リソースが定まってない
  - ふわっとしたJSONシリアライザ ( [rabl](https://github.com/nesquena/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](https://github.com/rails-api/active_model_serializers/))
  - API設計レビュー、設計力の向上、設計の標準化
- クライアントも、ドメインモデルの手前にJSONをマップする層を持っている
  - 腐敗防止層 (参考: [@yanzm さんの記事](http://y-anz-m.blogspot.jp/2017/03/droidkaigi-2017_9.html) )

---

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

- 役割が結構かぶる
  - Server: JSON Schema / Serializer
  - BFF: GraphQLType
  - Client: 腐敗防止層
- 必要かどうかは、チームの距離感の問題でもある
  - 結合度
  - そこまで遠くする必要はない

---

# 落穂拾い

---

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

- ひとまず、更新系はBFFに持っていない
- 更新系のオーケストレーションは、今後もしない
- やるとしたら「このボタン(アクション)に対応するエンドポイントをBFFで持つ」
  - GraphQLではちょっと難しい

---

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

- GraphQLでは、レスポンスは errors または data のキーを返す
- 複数リソースを同時に取得しようとして、1つが失敗した場合、errorsだけ返すのが仕様
- BFFでは、成功したリソースは返しつつ、失敗したリソースはその理由を返す必要がある
  - つまり errors は基本使えない

---

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


```json
{
  "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