はじめに(導入)
業務で BFF(Backend For Frontend) を実装するために、TypeScript(以下 TS)を書く機会がありました。
「フロントからリクエストを受け取り、バックエンドへ流してレスポンスを返す」
――処理の流れ自体は抽象的には理解できていました。
ただ、実際のコードを見ると何をしているのか全く分からない。
研修で Java はある程度書けるようになっていたので、バックエンドのコードは読めます。しかし BFF は TS。
.pipe(
map((res) => res.data),
catchError(catchErrorHandler),
)
これを初めて見たとき、正直な感想は
「アラビア語を見ているのと大差ない」
でした…
私たちの現場では
- GraphQL
- RxJS
- TypeScript
を組み合わせた構成を採用しています。
TS の基礎すらない状態では、概念が一気に押し寄せてきて理解が追いつかない。
そんな中、エンジニアの友人から言われた一言も刺さりました。
「これからエンジニアとしてやっていくなら、TS と Python は必須だよ」
今はそんな焦りから、少し解放されたので
今回はかなり 限定的なテーマ ではありますが、
- TS が読めない
- Observable が何をしているのか分からない
そんな 過去の自分 のような人の助けになればと思い、
フロントからの処理の流れ と、その中で使われる Observable について深掘りしていきます。
また、現在うちの現場では Observableは不要だという話題もあがっていたので、その真意についても考察しています。
フロントからの入力値を受け取る際には何が起きているのか
まずは 超ざっくりした流れ です。
- フロントエンドが API を呼ぶ
- BFF がリクエストを受け取る
- BFF がバックエンド API を呼ぶ
- バックエンドのレスポンスを加工する
- フロントへ返す
BFF の役割は、
- フロントに最適な形に変換する
- バックエンドの複雑さを隠蔽する
ことです。
ここまでは Java の Controller + Service とかなり似ています。
問題は 「レスポンスをどう扱っているか」。
TS の BFF では、
- Promise
- Observable(RxJS)
という仕組みがよく登場します。
TSの基礎:Promise を理解する
まずは Promise からです。
Promise は一言でいうと、
「将来、値が返ってくることを約束するオブジェクト」
です。
// fetchUser関数はPromiseを返す
// Promise = 「将来値が返ってくることを約束するオブジェクト」
function fetchUser(): Promise<string> {
return new Promise((resolve, reject) => {
// 1秒後にresolveで値を返す
// この瞬間にPromiseは完了状態(fulfilled)になる
// もしここで reject(error) だった場合は完了状態(rejected)になる
setTimeout(() => {
resolve("user-data"); // ←Promiseの完了を決定する箇所
}, 1000);
});
}
// fetchUserを呼び出す
const promise = fetchUser();
// thenで値を受け取る
// ここ以外で定義したものに加工はできない
// Promiseは一度完了すると再度新しい値を流すことはできない
promise
.then((data) => {
// data = "user-data"
// ここで初めて値を受け取り、加工や次の処理が可能
return data.toUpperCase();
})
.then((result) => {
// result = "USER-DATA"
console.log(result);
});
// 別のthenでも同じ結果が使えるが、新しい値は自動では流れない
promise.then((data) => console.log("別のthenでも同じ値:", data));
説明
-
resolve()/reject()が呼ばれた時点で Promise は完了状態になる -
.then()は「完了した結果を受け取るコールバック」であり、ここで初めて値を加工可能 - Promise は一度完了したら、新しい値を自動的には流さない
- そのため「値を受け取った処理はここ以外では加工できない」という制約がある
まとめ
- Promise = 1回きりの結果
- 値を受け取るのは
.then()の中だけ - それ以外では加工・再利用はできない
Observableとは何か(感覚的な理解)
Observable は、
「データが流れてくる蛇口」
だと考えると分かりやすいです。
- 水道:蛇口をひねるまで水は出ない
- Observable:subscribe するまでデータは流れない
また、水がいつ出てくるか分からないように、Observable から値が流れてくるタイミングも 非同期 です。
emit(エミット)とは?
emit とは、
Observable が新しい値を下流に流すこと
です。Observable の中で next() が呼ばれるたびに、新しい値が emit されます。
Observableを定義して pipe で流してみる
import { Observable, map, filter } from 'rxjs';
// Observable = 「データが流れてくる蛇口」
const source$ = new Observable<number>((observer) => {
observer.next(1); // emit: 値を下流に流す
observer.next(2); // emit
observer.next(3); // emit
observer.complete(); // ストリームの終了
});
// pipeで加工1: 値を10倍にする
const multiplied$ = source$.pipe(
map((value) => value * 10) // mapで加工
);
// pipeで加工2: 偶数だけ残す
// ※ ここが Promise ではできない操作(途中で加工や分岐を追加できない)
const even$ = multiplied$.pipe(
filter((value) => value % 20 === 0) // filterで加工
);
// subscribeで値を受け取る
even$.subscribe((result) => {
console.log(result); // 20, 40
});
ポイント:
- Observable は 定義しただけでは何も起きない
-
subscribeした瞬間に処理が始まる -
pipeは「加工ライン」
pipe は Observable を加工する場所
流れを図にすると次のようになります。
[入力ストリーム] → pipe(map, catchError) → [出力ストリーム]
- 入力ストリーム:元の Observable
- pipe:データ加工レーン
- 出力ストリーム:加工後の新しい Observable
pipe の中でやっているのは、
値そのものを加工しているのではなく、
Observable(ストリーム)を別の Observable に変換している という点です。
map / catchError の役割
-
map: 流れてきた値を別の形に変えて下流に流す(フィルターや加工) -
catchError: ストリームの途中でエラーが起きた場合に別の流れに切り替える
なぜ自分たちの現場では Observable が不要となったのか
いつかの会議で
「あれ、うちの現場ってこのメリット、ほとんど使ってないのでは?」
という疑問が浮かびました。
Observable の代表的なユースケース:
- 複数 API を並列で呼ぶ
- 非同期処理を合成する
- ストリーム的に加工する
しかし、私たちの現場では、
- 複数 API の集約・加工は バックエンド側で完結
- BFF は 1つの API を呼んで返すだけ
という構成でした。
つまり、
BFF で複雑な非同期処理を組み立てる必要がなかった
のです。
この構成であれば、
- 非同期処理は1回
- レスポンスも1回
なので、Promise で十分ということなのではないかと思いました。
それでも Observable が使われていた理由(考察)
では、
「Observable は完全に不要なのか?」
という疑問が浮かびます。
私たちのプロジェクトでは、datarest(自動生成コード)が Observable を返す設計になっていました。
なぜこの設計が選ばれたのかは、正直なところ 断定できません。
ただ、コードや構成を眺める中で、次のような意図があったのではないかと考えました。
- プロジェクト全体で RxJS を使う前提を揃えたかった
- 将来 BFF の責務が増える可能性を見据えていた
- エラーハンドリングやレスポンス加工の書き方を統一したかった
あくまで推測ですが、「今は不要でも、設計として Observable を選ぶ」という判断だったのかもしれません。
現在はこの設計をした方は現場にいませんが、
なぜ当時 Observable を選んだのか、いつか直接聞いてみたいテーマです。
まとめ
- BFF はフロントとバックエンドの通訳
- Promise は「1回きりの結果」を扱うもの
- Observable + pipe は「流れてくるデータを、どう加工して渡すか」を扱う仕組み
- Observable が不要に見えたのは、設計上その強みが活きなかっただけ
最初は本当に意味不明でしたが、
Observable はデータの川
と捉えたことで、コードが読めるようになりました。
この記事が、
- TS が読めなくてつらい人
- Observable が怖い人
の助けになれば嬉しいです。