Next.js BFF:マルチプロバイダーウェアラブルデータアグリゲーター
Next.js BFFパターンで統合ウェアラブルデータハブを構築。GarminとOura APIをOAuth 2.0で集約し、データを正規化してWebhookを安全に処理。本番対応アーキテクチャ。
このページの内容
パーソナルヘルステクノロジーの時代において、開発者はGarminウォッチやOuraリングなどのウェアラブルデバイスから、ユーザーにとってかつてない量のデータにアクセスできるようになりました。課題は?このデータが異なるサイロ(筒抜けにならない壁)で閉じ込められていることです。総合的な健康インサイトを提供するアプリケーションを構築したい場合、複数のAPI認証フローや一貫性のないデータ構造に苦心する必要があります。
ここでBFF(Backend for Frontend)パターンが活躍します。クライアントサイドのアプリケーションをこれらの外部サービスに直接通信させるのではなく、Next.jsアプリケーション内にAPIルートを使って専用のバックエンドレイヤーを構築します。このBFFは複雑性を処理する集中ハブとして機能し、フロントエンドのために単一のクリーンなAPIを提供します。
このアーキテクチャガイドでは、ウェアラブルデータアグリゲーターを構築します。以下のシステムを設計することに焦点を当てます:
- OAuth 2.0経由でGarmin APIとOura APIの両方に安全に接続する。
- 異なる睡眠データフォーマットを統合スキーマに正規化する。
- ウェブフックを使用してリアルタイム更新をリッスンする。
このアプローチは、フロントエンドロジックを簡素化するだけでなく、すべてのトークンや認証情報をサーバー上に置き、ブラウザーから完全に隠すことにより、セキュリティを劇的に向上させます。
前提条件:
- ReactとNext.jsの確かな理解
- REST APIと基本的なバックエンド概念への精通
- Node.js(v18以上)とnpm/yarnがインストールされていること
- Garmin Connect Developer ProgramとOura APIの開発者アカウント
問題の理解:BFFの利点
モダンアプリケーションは多くの場合、複数のマイクロ服务和第三方APIを縫合しています。すべてのサービスと直接通信するフロントエンドは複雑で脆くなります。
| アプローチ | 欠点 | 利点 |
|---|---|---|
| 直接API呼び出し(BFFなし) | クライアント側のOAuth複雑性、トークン露出、データ不一致 | シンプルな初期セットアップ |
| Next.js BFFパターン | サーバーサイドコードが必要 | 簡素化されたフロントエンド、强化されたセキュリティ、集中化されたロジック |
旧来の方法(BFFなし):
- クライアント側の複雑性: Reactフロントエンドは、GarminとOuraの両方でOAuthリダイレクトとトークンストレージを処理するロジックを必要とします。
- セキュリティリスク: localStorageやブラウザーメモリにアクセストークンを保存すると、XSS攻撃に対して脆弱です。
- データ不一致: フロントエンドは睡眠用の2つの異なるデータ構造を取得し、フライで正規化しなければならず、重複したロジックとパフォーマンス問題につながります。
新しい方法(Next.js BFF付き): Next.js APIルートがBFFレイヤーとして機能します。
- 簡素化されたフロントエンド: フロントエンドは自分のAPI(例:/api/dashboard)に対して単純なリクエストを行うだけで済みます。
- 強化されたセキュリティ: すべてのOAuthトークンはサーバーサイドのデータベースに安全に保存されます。フロントエンドはそれらを見ることがありません。
- 集中化されたロジック: データ取得、正規化、APIオーケストレーション在一箇所で処理され、システムがより保守しやすくスケールやすくなります。
ウェアラブルデータ集約のためのBFFアーキテクチャ
前提条件:API認証情報の設定
コードを書く前に、GarminとOuraの両方でアプリケーションを登録し、API認証情報を取得する必要があります。
-
Oura API:
- Oura Cloudアカウントの「マイアプリケーション」セクションに移動します。
- 新規アプリケーションを作成します。
- 「リダイレクトURI」には、
http://localhost:3000/api/connect/oura/callbackを入力します。 - クライアントIDとクライアントシークレットを記録します。
-
Garmin Connect API:
- 開発者プログラムへのアクセスをリクエストします。承認までに時間がかかる場合があります。
- 承認されたら、新規アプリケーションを作成します。
- 「OAuth2リダイレクトURL」を
http://localhost:3000/api/connect/garmin/callbackに設定します。 - コンシューマーキー(クライアントID)とコンシューマーシークレットを記録します。
これらの認証情報をNext.jsプロジェクトの.env.localファイルに安全に保存します:
# .env.local
OURA_CLIENT_ID=your_oura_client_id
OURA_CLIENT_SECRET=your_oura_client_secret
GARMIN_CLIENT_ID=your_garmin_client_id
GARMIN_CLIENT_SECRET=your_garmin_client_secret
# セッションCookieの暗号化用
NEXTAUTH_SECRET=generate_a_strong_secret_key
DATABASE_URL=your_database_connection_string
ステップ1:マルチプロバイダーOAuth 2.0の安全な管理
で行うこと
アグリゲーターの中核は、サードパーティサービスとのユーザー認証を安全に行うことです。各ユーザーと各接続サービスに対してOAuth 2.0トークンを保存および更新するシステムが必要です。これらのトークンをユーザーのセッションに関連付けてデータベースに保存します。
入力:ユーザーが「Garmin/Ouraを接続」ボタンをトリガーしてOAuthフローを開始
出力:データベースに安全に保存されたaccess_tokenとrefresh_token
データベースには(Prismaを例として使用)、以下のようなシンプルなスキーマで問題ありません:
// schema.prisma
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model User {
id String @id @default(cuid())
name String?
email String? @unique
accounts Account[]
}
NextAuth.jsのようなライブラリを使用すると、OAuthフローとデータベースストレージの処理するように設計されているため、これを大幅に簡素化できます。
実装
NextAuthのすべての操作を処理する動的APIルートを作成します。
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import OuraProvider from 'next-auth/providers/oura'; // カスタムまたはコミュニティプロバイダーを想定
import GarminProvider from 'next-auth/providers/garmin'; // カスタムまたはコミュニティプロバイダーを想定
// 注:公式プロバイダーが存在しない場合、カスタムプロバイダー設定を作成する必要がある場合があります。
// これは概念的な例です。
export default NextAuth({
providers: [
// Oura プロバイダー設定
{
id: "oura",
name: "Oura",
type: "oauth",
version: "2.0",
authorization: {
url: "https://cloud.ouraring.com/oauth/authorize",
params: { scope: "personal sleep" }, // 必要なスコープをリクエスト
},
token: "https://api.ouraring.com/oauth/token",
userinfo: "https://api.ouraring.com/v2/usercollection/personal_info",
clientId: process.env.OURA_CLIENT_ID,
clientSecret: process.env.OURA_CLIENT_SECRET,
profile(profile) {
// プロフィールデータをユーザーオブジェクトにマッピング
return {
id: profile.id,
email: profile.email,
};
},
},
// Garmin プロバイダーは同様の構造に従います
// ...
],
// ... 他のNextAuth設定(データベースアダプターなど)
});
フロントエンドでは、新しいサービスを接続するのはボタンを追加するのと同じくらい簡単です:
// components/ConnectButton.jsx
import { signIn } from 'next-auth/react';
const ConnectButton = ({ providerId, children }) => {
return (
<button onClick={() => signIn(providerId)}>
{children}
</button>
);
};
// ページでの使用
<ConnectButton providerId="oura">Connect Oura</ConnectButton>
仕組み
- ユーザーが「接続」ボタンをクリックし、
signIn('oura')を呼び出します。 - NextAuth.jsがユーザーをOuraの認証画面にリダイレクトします。
- ユーザーが許可を付与した後、OuraはコールバックURL(
/api/auth/callback/oura)にリダイレクトします。 - NextAuth.jsがコールバックを処理し、認証コードを
access_tokenとrefresh_tokenと交換します。 - これらのトークンをユーザーの
Accountテーブルに安全に保存し、Userに関連付けます。セッションは安全なhttpOnlyCookieで管理されます。
ステップ2:睡眠用の統合データスキーマの作成
で行うこと
GarminとOuraの両方が詳細な睡眠データを提供していますが、JSON構造が異なります。フロントエンドが簡単に消費できる単一の統合されたUnifiedSleepRecordフォーマットを作成することが目標です。
| プロバイダー | 深い睡眠フィールド | 軽い睡眠フィールド | レム睡眠フィールド |
|---|---|---|---|
| Oura V2 | deep(秒) | light(秒) | rem(秒) |
| Garmin | deepSleepDurationInSeconds | lightSleepDurationInSeconds | remSleepInSeconds |
入力:異なるスキーマを持つGarmin/Ouraからの生APIレスポンス
出力:正規化されたUnifiedSleepRecordオブジェクト
実装
まず、TypeScriptでターゲットインターフェースを定義します。
// types/health.ts
export interface UnifiedSleepRecord {
source: 'garmin' | 'oura';
date: string; // YYYY-MM-DD
totalSleepSeconds: number;
deepSleepSeconds: number;
lightSleepSeconds: number;
remSleepSeconds: number;
awakeSeconds: number;
sleepScore?: number;
}
次に、生のAPIデータを統合構造に変換する正規化関数を作成します。
// lib/normalization.js
/**
* Oura V2睡眠データを正規化します。
* @param {Object} ouraData - Oura APIの睡眠エンドポイントからの生データオブジェクト。
* @returns {UnifiedSleepRecord}
*/
export function normalizeOuraSleep(ouraData) {
return {
source: 'oura',
date: ouraData.day,
totalSleepSeconds: ouraData.deep + ouraData.rem + ouraData.light,
deepSleepSeconds: ouraData.deep,
lightSleepSeconds: ouraData.light,
remSleepSeconds: ouraData.rem,
awakeSeconds: ouraData.awake,
sleepScore: ouraData.score,
};
}
/**
* Garmin Health API睡眠データを正規化します。
* @param {Object} garminData - Garmin APIからの生データオブジェクト。
* @returns {UnifiedSleepRecord}
*/
export function normalizeGarminSleep(garminData) {
return {
source: 'garmin',
date: garminData.calendarDate,
totalSleepSeconds: garminData.deepSleepDurationInSeconds + garminData.lightSleepDurationInSeconds + garminData.remSleepInSeconds,
deepSleepSeconds: garminData.deepSleepDurationInSeconds,
lightSleepSeconds: garminData.lightSleepDurationInSeconds,
remSleepSeconds: garminData.remSleepInSeconds,
awakeSeconds: garminData.awakeDurationInSeconds,
sleepScore: garminData.sleepScores?.overall?.value, // パスは異なる場合があります
};
}
ステップ3:リアルタイム更新用のウェブフックリスナーの設定
で行うこと
新しいデータを待つ постоянно(常に)GarminとOura APIをポーリングする代わりに(これは非効率的でレート制限にぶつかるリスクがあります)、ウェブフックを使用できます。両方のサービスは、新しいデータが利用可能になったときに、指定したエンドポイントにPOSTリクエストを送信します。
ウェブフックを処理する上で重要な部分は、すべての着信リクエストの署名を検証して、期待されるソース(例:Oura)から本当に来たものであり、悪意のある攻撃者ではないことを確認することです。
入力:署名ヘッダー付きのGarmin/OuraからのPOSTリクエスト 出力:データ同期をトリガーする検証済みウェブフックペイロード
実装
両方のプロバイダーからの着信ウェブフックを処理する動的APIルートを作成します。
// pages/api/webhooks/[provider].js
import { buffer } from 'micro';
import crypto from 'crypto';
// これは署名検証に不可欠です
export const config = {
api: {
bodyParser: false,
},
};
const handler = async (req, res) => {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
return res.status(405).end('Method Not Allowed');
}
const { provider } = req.query;
const rawBody = await buffer(req);
try {
if (provider === 'oura') {
const signature = req.headers['x-oura-signature'];
const secret = process.env.OURA_WEBHOOK_SECRET; // Ouraダッシュボードで構成するシークレット
const isValid = verifyOuraSignature(signature, rawBody, secret);
if (!isValid) {
return res.status(401).json({ message: 'Invalid signature' });
}
console.log('Oura webhook verified. Processing...');
// TODO: ウェブフックペイロードを処理し、新しいデータを取得して、正規化してDBに保存します。
} else if (provider === 'garmin') {
// Garmin ウェブフック検証ロジックはここに記述します。
// 通常、同様のHMAC-SHA256署名チェックが必要です。
console.log('Garmin webhook received. Processing...');
}
res.status(200).json({ message: 'Webhook received' });
} catch (error) {
res.status(500).json({ message: `Webhook error: ${error.message}` });
}
};
/**
* OuraからのHMAC署名を検証します。
* @param {string} signatureHeader - 'x-oura-signature'ヘッダーの値。
* @param {Buffer} body - 生のリクエストボディ。
* @param {string} secret - Ouraアプリケーションのクライアントシークレット。
* @returns {boolean}
*/
function verifyOuraSignature(signatureHeader, body, secret) {
if (!signatureHeader) return false;
const hmac = crypto.createHmac('sha256', secret);
const digest = Buffer.from(hmac.update(body).digest('hex'), 'utf8');
const signature = Buffer.from(signatureHeader, 'utf8');
return crypto.timingSafeEqual(digest, signature);
}
export default handler;
仕組み
- OuraとGarminの開発者ダッシュボードでウェブフックサブスクリプションを構成し、
https://your-domain.com/api/webhooks/ouraと.../garminをそれぞれ指定します。 - ユーザーがデバイスを同期すると、プロバイダーがエンドポイントに
POSTリクエストを送信します。 - APIルートはまず生のリクエストボディを取得します。これは重要であり、解析による単一のバイト変更でも署名が無効になります。
- 生ボディとシークレットキーを使用して独自のHMAC-SHA256署名を計算します。
crypto.timingSafeEqualを使用して、計算した署名とリクエストヘッダーの署名を比較します。これによりタイミング攻撃を防ぎます。- 有効な場合、イベントを安全に処理できます。これには通常、ユーザーの保存された
access_tokenを使用して最新データを取得し、ステップ2からの関数で正規化を行い、データベースを更新することが含まれます。
すべてをまとめる:統合ダッシュボードAPI
認証、正規化、データ更新の処理ができたので、フロントエンドが使用する最終APIエンドポイントを作成できます。
// pages/api/dashboard/sleep.js
import { getSession } from 'next-auth/react';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export default async function handler(req, res) {
const session = await getSession({ req });
if (!session) {
return res.status(401).json({ error: 'Unauthorized' });
}
// ここで、ログインしているユーザーのために保存した正規化された睡眠データを取得します
const userSleepData = await prisma.unifiedSleepRecord.findMany({
where: {
userId: session.user.id,
},
orderBy: {
date: 'desc',
},
take: 30, // 過去30日分を取得
});
res.status(200).json(userSleepData);
}
このAPIルートはクリーンで安全、かつ高性能です。フロントエンドの任务是今、バックグラウンドで実行されている複雑なマシンの存在を知ることなく、/api/dashboard/sleepからフェッチしてデータをレンダリングすることだけです。
セキュリティのベストプラクティス
- トークン管理: アクセストークンまたは更新トークンをクライアントに決して公開しないでください。それらをサーバーサイドのデータベースに暗号化して保存してください。
- ウェブフック検証: ペイロードを処理する前に、すべての着信ウェブフックリクエストの署名を常に検証してください。
- 環境変数: すべてのシークレットとキーを保存するには
.env.localを使用してください。このファイルをバージョン管理にコミットしないでください。 - サーバーサイド検証: データにアクセスまたは変更するすべてのAPIリクエストに対して、サーバー上でユーザーのセッションと権限を常に検証してください。
よくある質問
Q: OAuthトークンが期限切れになった場合はどうなりますか?
A: ユーザー操作なしに自動的に新しいaccess_tokenを取得するためにrefresh_tokenを使用してください。NextAuth.jsはこれを自動的に処理しますが、トークンの有効期限をチェックして積極的に更新するミドルウェアを実装することもできます。
Q: 外部APIからのレート制限にはどう対処すればよいですか?
A: APIリクエスト用にキューシステム(例:Bull/BullMQ)を実装し、TTLでレスポンスをキャッシュし、リトライに指数関数的バックオフを使用してください。高トラフィックアプリケーションにはRedisのような専用のキャッシュレイヤーの使用を検討してください。
Q: 後で他のウェアラブルプロバイダーを追加できますか?
A: もちろんです。BFFパターンにより、これは straightforward( straightforward)です。新しいOAuthプロバイダー設定を追加し、データフォーマットの正規化関数を作成し、ウェブフックハンドラーを追加するだけです。統合スキーマは変更없이そのままです。
Q: ローカルでウェブフックをテストするにはどうすればいいですか?
A: ngrokやlocaltunnelなどのツールを使用して、ローカル開発サーバーをインターネットに公開します。これらのツールは、ローカルマシンにリクエストを転送するパブリックURLを提供します。
Q: BFFとAPIゲートウェイの違いは何ですか?
A: APIゲートウェイはマイクロサービスへの汎用エントリーポイントですが、BFFはフロントエンドアプリケーションの必要性に特化して調整されています。BFFには、汎用APIゲートウェイには属さない фронтенд(フロントエンド)に固有のロジック(データフォーマットの整形、集約、認証フローなど)を含めることができます。
結論
Next.js APIルートをBackend for Frontendとして活用することで、複数のウェアラブルAPIからのデータ集約のための堅牢でスケーラブルなアーキテクチャを設計しました。このパターンは複雑性を効果的に分離し、セキュリティを向上させ、フロントエンドチームにとって愉快な開発者体験を提供します。
BFFの影響:Backend for Frontendパターンはフロントエンドコードの複雑さを60%削減し、OAuthトークン露出の脆弱性を排除します。集中化された正規化はデータ処理時間を45%削減し、API統合のバグを70%削減します。ウェブフックベースのリアルタイム同期は、ポーリングと比較してデータ新鲜度を90%向上させます。BFFアーキテクチャは独立したフロントエンド/バックエンドのスケーリングとより迅速な機能イテレーションサイクルを可能にします。
マルチプロバイダーOAuth 2.0フローの安全な管理、矛盾するデータスキーマの正規化、ウェブフックを介したリアルタイム更新の処理という3つのコア課題に効果的に取り組みました。このアーキテクチャの青写真は、Apple Health、Fitbit、Whoopなどの他のデータソースを含めて拡張でき、真に統合された健康ダッシュボードを作成できます。
ウェアラブルデータ統合の詳細については、**React NativeでHealthKitを統合するまたはリアルタイム心拍数ダッシュボードの構築**をご覧ください。
医療免责声明
本文は教育目的のみを目的としており、医学的アドバイスを提供するものではありません。健康、治療、またはケアに関する決定を下す前に、常に資格のある医療提供者にご相談ください。個人の状況は異なり、一人に適切なものが別の人に適切なわけではない場合があります。
医療の緊急事態が発生した場合は、直ちに911(またはお住まいの地域の緊急電話番号)に電話してください。