本記事はQiita Advent Calender 2025 わたなべの11日目です!
11日目は、私渡邊が作成します!
今回は、アドカレ4日目に書いた Vercel Workflow絵日記の続きです!
バックエンド側での処理をフロントへリアルタイムに連携することで、UXを向上させることについて書いてみます!
![]()
絵日記らくちんだわ~
また、workflow を実装していて困ったことにも遭遇しているので、関連したTipsも記述してみます!
client side, server side, databaseの連携がいまいちわからないよって方には特に読んでいただけると、お役に立てるところがあるかもしれません!
前回までのあらすじ| vercel workflow で絵日記を作った
絵日記を作成するところは次のようなフローです。
1) 箇条書きを送信
ユーザー → 「今日あったこと」を箇条書きで送る
2) AIが文章を下書き
アプリ → AIが絵日記の文章を自動で考え、少しずつ画面に表示
3) ユーザーが文章を直す
ユーザー → 気になるところを手直しして送信
ここでワークフローは入力待ち
4) AIが画像とタイトルを生成
アプリ → 直した文章をもとにAIが絵を作り、タイトルも自動生成
絵の生成とタイトルの生成を並列で実行
5) 完成して保存
アプリ → 文章・画像・タイトルを保存し、一覧や詳細画面で見られるようにする
絵日記作成のフローの中で、ユーザーに通知する部分は次の通り。
1. workflowがどのような状態か
日記を生成している,ユーザーの修正まち,絵を生成している,完了というworkflowの状態をユーザーに通知する。
これはシンプルですね。今、ワークフローが何をしているのかをフロントに表示することで、ユーザーに安心感を与えたいですね。状態がわかればよいので、緊急性が高いわけではないです。
2. 文章生成の状況
生成された文章がいきなり表示されるのではなく、チャットUIのようにストリーミングが随時表示される。
通常であれば、streamableなAPIにPOSTを送ると、結果がstreamingで返ってくるので、それをフロントに随時反映すればよいです。
ですがworkflowの場合、開始するためのAPIにリクエストを送っても、帰ってくるのはworkflowが完了した結果のみ。途中で発生している処理の中身を受け取る手段がないため一工夫必要となります。
途中経過をフロントに連携する
workflow開始、結果を受け取っているcomponentはこちら。
長いので200行でちょん切れてます
ここで、下記pollingとSSEを処理しています。
1. なんとなくわかればうれしいもの|polling
workflowの状態については何となく今の状況がわかればよい、止まっていないことがユーザーに伝わればよい、という風に考えてpollingで取得しました。
すなわち、状態をdbに記載し、一定時間ごとにフロントから状態を取得する実装をしています。
const pollStatus = useCallback(async (id: string) => {
try {
const res = await fetch(`/api/workflow/status?workflowId=${id}`);
if (!res.ok && res.status !== 202) throw new Error("Status fetch failed");
const json = (await res.json()) as WorkflowStatus;
setStatus(json);
if (json.status === "COMPLETED") return;
// ここがポーリング
pollTimer.current = setTimeout(() => pollStatus(id), 2000);
} catch (err) {
console.error(err);
pollTimer.current = setTimeout(() => pollStatus(id), 4000);
}
}, []);
通常時は2秒ごと、エラーが出たときは4秒待ったうえで状態をAPI(dbに記録された状態をとるAPI)から取得しています。
const res = await fetch(`/api/workflow/status?workflowId=${id}`);
2. リアルタイムに連携したいもの|SSE
SSE(Server Sent Event)を利用してストリーミングをフロントで受信しています。
2.1. SSE?
たぶん、私の記事を読まれる方は初めて聞いたという方も多いと思います。server sent eventの名前の通り、サーバーからクライアント(フロント)に1方向でデータを送り続ける仕組みです。
なじみのあるAPIはサーバーにリクエストを送り、結果が返ってくるのに対し、SSEは一方的に長時間データが流れてきます。これにより生成されるストリームを受信、都度UIを更新することでよくあるチャットアプリのような流れのあるテキスト更新を実装しています。
2.2. REST APIとSSE
REST APIの場合往復して終わり
SSEは一度接続するとサーバーからずっとデータが来る
2.3. 今回の実装
workflowを開始するAPI
SSEを開始するAPI
- workflowを開始しつつSSE接続も開始
- フロントでストリーミングを受け取りinput欄に反映
- それをユーザーが修正してhookで再度ワークフローに流す
という流れで、UIをストリーミングで受信しつつworkflowを動かし続けています!
前の記事のhookのところにリンク
参考|websocket(今回は未使用)
リアルタイムでフロントに反映する技術としてwebsocketというものもあります。これは、SSEがサーバーからの一方通行だったのに対して、双方向でやり取りができる技術です!
例えば、チャットルームだったり、miroのような共同編集できるホワイトボードで使われています。
あとは、supabaseのrealtime listenerのように、データベースが更新されたらその内容をフロントに反映する機能でも利用されています。
今回、workflow側への送信はhookを通じて実施しており、双方向性が必要ないため利用しておりません。(先日更新したBunのチャットアプリはwebsocket使っています)
dbとのやり取りで困った
今回困って解決できず、別な手段に切り替えたのがdbとのやり取りです。
具体的には、ORMとして初期はprismaを利用していましたが、本番の実行ができず、drizzleに変更するという対応が発生しました。
1. 開発環境では問題なく動作した
大前提として、prismaはNode.jsで動作します。
次のリンク先にあるprisma-engineというRustで書かれた機能を実行するために、Node.jsで呼び出す必要があるためです。
prisma-engine Rustで書かれています
はじめ私はworkflowの中にprisma clientを記述したため動きませんでした (阿保ですね)。workflowはVercel QueuesというEdge runtimeで動作するためです。
prisma依存のコードをすべてNext.jsのAPI routeに書き直し、workflowからAPIを実行するという構成に修正したことで、ローカルでは問題なく動いていました。
2. 本番環境で動かないprisma
もう少し早い段階でdeploy先の動作を確認するべきだったんだと思います。一通り作り終わって、差分をvercelにdeployしたのですが、フロントで次のエラーが発生し続けました。
Error [PrismaClientInitializationError]:
Invalid prisma.diary.findMany() invocation:
Prisma Client could not locate the Query Engine for runtime "rhel-openssl-3.0.x".
We detected that you are using Next.js, learn how to fix this: https://pris.ly/d/engine-not-found-nextjs.
This is likely caused by tooling that has not copied "libquery_engine-rhel-openssl-3.0.x.so.node" to the deployment folder.
Ensure that you ran prisma generate and that "libquery_engine-rhel-openssl-3.0.x.so.node" has been copied to "node_modules/.prisma/client".
ローカルの開発環境ではprismaは問題なく動作していたので困惑しました。vercel workflowの中身のコードまではちゃんと読んでいないので確証はないのですが、runtimeがedge強制になるっぽいような挙動で、そうなるとAPI routeにprisma clientを実行するようにしてもQuery Engineが配置できずエラーになるのかもしれません。
3. workflowを使うときはdrizzleを使おう!
prismaからdrizzleというORMに変更することを考えました。
エラー内容からdrizzleに変更すれば動くことは間違いないと思っていました。drizzleはprismaのようなQuery Engineはなく、TypeScriptで書かれていますのでEdge runtimeで動作します。
ただ、ここまでprismaで進めてきて、全部書き換えるのはやばそうだと思い躊躇したのですが、失敗したら戻せばよいかと思い、進捗をgit commitしてcodexにお願いしてみました。
結果、5分くらいで終わり問題なく動作しました。
workflowを利用するときのORMはdrizzleを利用するのが無難なようです。
実装難しそう・・・と思っても大丈夫!
コードが長々と、実装するの大変そう、という方もいらっしゃるかもしれませんが、大丈夫!私もよくわかってません!
よくわかってはいませんが
- 用途に応じた接続方法を知っておくこと
- 認識祖語なく伝えるために正しい名称を知っておくこと
- その上で、試しにLLMでよいので実装してみること
- 吐き出されたコードを読みながら自分の中で解釈すること
これを繰り返すうちに、自然とやりたいこともできるようになっていくと思います。
まあ、私もプロエンジニアではないので間違っているところあるかと思いますが、その際はご指摘いただけるとうれしいです!
