ReactでサーバーサイドOura Ringデータ取得: Next.js & React Queryチュートリアル
Next.jsとReact QueryでOura Ring APIを統合する方法を学びます。OAuth 2.0、サーバーサイドデータフェッチ、SSRをカバーしたステップバイステップのチュートリアルです。
このページの内容
フック: ヘルステックは急速に成長しており、開発者はパーソナライズされたウェルネスアプリケーションを構築する最前線に立っています。Oura Ringのようなウェアラブルデバイスは、睡眠、読みどき、アクティビティという貴重なデータを提供し、こうした体験を可能にします。しかし、このデータを安全に効率的にウェブアプリケーションに統合することは、認証、サーバー状態管理、パフォーマンス最適化を含む複雑な課題です。
構築・学習内容: このチュートリアルでは、OAuth 2.0を使用してOura Ring APIに安全に接続するNext.jsアプリケーションを構築します。その後、TanStackのReact Queryを使用して、ユーザーの睡眠、読みどき、アクティビティスコアを取得、キャッシュ、表示します。サーバサイドレンダリング(SSR)アプローチに焦点を当て、データがクライアントにページが送信される前に、新鮮で安全かつ読み込み済みであることを確保します。
前提条件:
- Node.js(v18以降)
- ReactとNext.jsの基礎的な理解
- Oura RingとアクティブなOuraアカウント
- JavaScriptにおけるasync/awaitへの慣れ親しみ
開発者にとってなぜ重要か: このガイドは、モダンなウェブフレームワークでサードパーティAPIデータを安全に処理するという現実の課題に取り組みます。サーバー上でAPIキーとアクセストークンを管理するための堅牢なパターンを学び、サーバー上で全て管理することでクライアントへの露出を防ぎます。SSRとReact Queryをマスターすることで、ユーザーに非常にレスポンシブに感じられる高性能アプリケーションを構築できるようになります。
問題の理解
OuraのようなサードパーティAPIの統合には、いくつかの課題があります:
- 認証:Oura APIはOAuth 2.0を使用しており、これは強力でマルチステップの認証プロトコルです。、一時的なコードを長寿命のアクセストークンと交換するための安全でサーバーサイドのフローを実装する必要があります。
- セキュリティ:クライアントシークレットとユーザーのアクセストークンは非常に機密性が高いため、絶対にクライアントサイドに露出させてはいけません。Ouraへの全てのAPI呼び出しは安全なサーバー環境から発信される必要があります。
- データ取得と状態管理:データの取得にはボイラープレートが必要です。冗長なAPI呼び出しを避けるために、ロード状態、エラー状態、キャッシュを管理する必要があります。これを
useStateとuseEffectで手動で行うと、すぐに複雑になります。 - ユーザー体験:可能な限りロードスピナーを表示したくないですよね。データはページ読み込み時に存在している必要があり、これはNext.jsのサーバサイドレンダリング機能の良い活用事例です。
私たちのアプローチ—OAuthフローとAPIプロキシのためのNext.jsルートハンドラの使用、状態管理とSSRのためのReact Queryの組み合わせ—は、これらの課題を優雅かつ効率的に直接解決します。
前提条件
コーディングを開始する前に、Ouraアプリケーションをセットアップしましょう。
-
Ouraアカウントの作成:お持ちでない場合は、cloud.ouraring.comでサインアップしてください。
-
OAuthアプリケーションを登録する:
- APIアプリケーションセクションに移動します。
- 「新規アプリケーションを作成」をクリックします。
- 詳細を入力します。「リダイレクトURI」には
http://localhost:3000/api/auth/callback/ouraを入力してください。これはOAuthフローの重要なステップです。 - 作成すると、クライアントIDとクライアントシークレットが交付されます。これらを安全に保管してください!
-
環境変数の設定:
- Next.jsプロジェクトのルートに
.env.localというファイルを作成します。 - Oura認証情報とセッションデータの暗号化用のシークレットを追加します。
bash # .env.local OURA_CLIENT_ID=your_client_id_here OURA_CLIENT_SECRET=your_client_secret_here NEXT_PUBLIC_OURA_CLIENT_ID=your_client_id_here APP_SECRET='complex-secret-for-session-encryption' NEXT_PUBLIC_APP_URL='http://localhost:3000'注:クライアントIDはログフロー開始時にクライアント側で必要されるため、公開されています(
NEXT_PUBLIC_)。クライアントシークレットはサーバー上で安全に保持されます。 - Next.jsプロジェクトのルートに
-
新しいNext.jsアプリの作成と依存関係のインストール:
bash npx create-next-app@latest oura-react-query-app cd oura-react-query-app npm install @tanstack/react-query @tanstack/react-query-devtools iron-session@tanstack/react-query:サーバー状態管理のコアライブラリ。@tanstack/react-query-devtools:React Queryのキャッシュをデバッグするためのヘルプツール。iron-session:Ouraアクセストークンのようなセッションデータを保存するための、安全で暗号化されたHTTPOnly cookiesを作成するためのライブラリ。
ステップ1:OAuth 2.0フローの実装
最初の主要なタスクは、Ouraでユーザーを認証することです。これには3つの部分が必要です:ユーザーをOuraにリダイレクトすること、コールバックを処理すること、そしてアクセストークンを安全に保存すること。
何をするか
Next.jsで2つのAPIルートを作成します:
/api/auth/login/oura:このルートはOura認証URLを構築し、ユーザーをそこにリダイレクトします。/api/auth/callback/oura:Ouraがユーザーの承認後にここにリダイレクトします。このルートは受け取ったcodeをaccess_tokenと交換し、暗号化されたcookieに保存します。
実装
まず、セッション管理ユーティリティをセットアップしましょう。
// lib/session.js
import { getIronSession } from 'iron-session';
const sessionOptions = {
password: process.env.APP_SECRET,
cookieName: 'oura-session',
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
},
};
export function getSession(req, res) {
return getIronSession(req, res, sessionOptions);
}
次に、ログインルートです。このルートはシンプルにユーザーをOuraの同意画面にリダイレクトします。
// app/api/auth/login/oura/route.js
import { NextResponse } from 'next/server';
export async function GET() {
const authUrl = new URL('https://cloud.ouraring.com/oauth/authorize');
authUrl.searchParams.set('client_id', process.env.NEXT_PUBLIC_OURA_CLIENT_ID);
authUrl.searchParams.set('redirect_uri', `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/oura`);
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'daily'); // 日々の睡眠、アクティビティ、読みどきデータをリクエスト
return NextResponse.redirect(authUrl.toString());
}
最後に、コールバックルートです。ここが魔法が起こる場所です。
// app/api/auth/callback/oura/route.js
import { NextResponse } from 'next/server';
import { getIronSession } from 'iron-session';
export async function GET(request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
if (!code) {
// エラーを処理:コードが提供されなかった場合
return NextResponse.redirect(new URL('/?error=oauth_error', request.url));
}
// コードをアクセストークンと交換
const tokenResponse = await fetch('https://api.ouraring.com/oauth/token', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: `${process.env.NEXT_PUBLIC_APP_URL}/api/auth/callback/oura`,
client_id: process.env.OURA_CLIENT_ID,
client_secret: process.env.OURA_CLIENT_SECRET,
}),
});
if (!tokenResponse.ok) {
// エラーを処理:トークンの取得に失敗した場合
console.error('Failed to exchange code for token:', await tokenResponse.text());
return NextResponse.redirect(new URL('/?error=token_exchange_failed', request.url));
}
const tokenData = await tokenResponse.json();
// トークンを暗号化してcookieに保存
const session = await getIronSession(request.cookies, new NextResponse().cookies, {
password: process.env.APP_SECRET,
cookieName: 'oura-session'
});
session.accessToken = tokenData.access_token;
await session.save();
// ダッシュボードページにリダイレクト
return NextResponse.redirect(new URL('/dashboard', request.url));
}
仕組み
OAuth 2.0「Authorization Code」フローは安全な認証の標準です。
- アプリがユーザーをOuraの認証サーバーにリダイレクトします。
- ユーザーはOuraにログインし、私たちのアプリケーションが
dailyデータにアクセスする許可を与えます。 - Ouraは指定された
redirect_uri(/api/auth/callback/oura)へ一時的なcodeとともにユーザーをリダイレクトします。 - サーバーサイドのコールバックハンドラーがこの
codeを受け取ります затем сервер делает安全的服务器间POST-запрос к Oura的/oauth/token端点,包含我们的client_secret。 - Ouraはリクエストを検証し、
access_tokenを返します。 iron-sessionを使用して、このトークンを暗号化されたhttpOnlycookieに保存します。これはトークンが安全に保存され、XSS攻撃を軽減するためにクライアントサイドのJavaScriptからアクセスできないことを意味します。
ステップ2:React QueryでOuraデータを取得
アクセストークンを取得したので、ユーザーデータを取得できます。Oura APIへのプロキシとして機能する別のAPIルートを作成し、その後React Queryを使用してフロントエンドから呼び出します。
何をするか
- アクセストークンをセッションcookieから読み取り、公式Oura APIからデータを取得するAPIルート
/api/oura/daily-summaryを作成します。 _app.jsまたはlayout.jsでReact QueryのQueryClientProviderをセットアップします。- ダッシュボードページで
useQueryフックを使用して内部APIルートを呼び出し、データを表示します。
実装
まず、サーバーサイドのAPIプロキシ。このルートはアクセストークンを保護します。
// app/api/oura/daily-summary/route.js
import { NextResponse } from 'next/server';
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';
export async function GET() {
const session = await getIronSession(cookies(), {
password: process.env.APP_SECRET,
cookieName: 'oura-session',
});
if (!session.accessToken) {
return NextResponse.json({ error: 'Not authenticated' }, { status: 401 });
}
// YYYY-MM-DD形式で昨日の日付を取得
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split('T');
const ouraResponse = await fetch(
`https://api.ouraring.com/v2/usercollection/daily_activity?start_date=${dateStr}&end_date=${dateStr}`,
{
headers: {
'Authorization': `Bearer ${session.accessToken}`,
},
}
);
if (!ouraResponse.ok) {
console.error('Oura API Error:', await ouraResponse.text());
return NextResponse.json({ error: 'Failed to fetch data from Oura' }, { status: ouraResponse.status });
}
const ouraData = await ouraResponse.json();
return NextResponse.json(ouraData);
}
次に、React Queryプロバイダーをセットアップします。これにより、クエリクライアントがアプリ全体で利用可能になります。
// app/providers.js
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { useState } from 'react';
export default function Providers({ children }) {
const [queryClient] = useState(() => new QueryClient());
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
// app/layout.js
import Providers from './providers';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
最後に、useQueryを使用してデータを取得および表示するダッシュボードページを構築しましょう。
// app/dashboard/page.js
'use client';
import { useQuery } from '@tanstack/react-query';
const fetchOuraData = async () => {
const res = await fetch('/api/oura/daily-summary');
if (!res.ok) {
throw new Error('Network response was not ok');
}
return res.json();
};
export default function Dashboard() {
const { data, isLoading, isError, error } = useQuery({
queryKey: ['ouraDailySummary'],
queryFn: fetchOuraData,
});
if (isLoading) {
return <span>Loading...</span>;
}
if (isError) {
return <span>Error: {error.message}</span>;
}
// APIが期待される形式でデータを返すと想定
const summary = data?.data?.[0];
if (!summary) {
return <span>昨日のデータがありません。</span>
}
return (
<div>
<h1>{summary.day}のOuraデイリースマリー</h1>
<h2>読みどきスコア: {summary.score}</h2>
<ul>
<li>アクティビティスコア: {summary.contributors?.activity_balance}</li>
<li>睡眠スコア: {summary.contributors?.recovery_index}</li>
</ul>
</div>
);
}
仕組み
useQueryはReact Queryの魔法です。一意のqueryKeyとqueryFn(データを返す非同期関数)を受け取ります。React Queryは自動的に以下を処理します:
- キャッシュ:このページから移動して戻ってきた場合、バックグラウンドで新鮮な取得が行われている間にデータがキャッシュから即座に提供されます。
- 読み込みとエラー状態:
isLoadingとisErrorブール値でUIロジックが簡素化されます。 - バックグラウンド再取得:ウィンドウがフォーカスされたときに自動的にデータを再取得でき、UIを同期状態に保ちます。
ステップ3:サーバサイドレンダリング(SSR)との統合
クライアントサイドのデータ取得アプローチは動作しますが、ユーザーに「Loading...」メッセージが表示されます。もっと良くできます。Next.jsのgetServerSidePropsとReact Queryのプリフェッチ機構を使用して、サーバーでデータを取得し、完全なHTMLをレンダリングできます。
何をするか
サーバーで最初にデータを取得するためにダッシュボードページをServer Componentを使用するようにリファクタリングします。React Queryはこれを「水和」(hydration)と呼ばれるメカニズムでサポートしています。サーバーでプリフェッチし、キャッシュを「脱水」(シリアライズ可能な形式に変換)し затемクライアントで「再水和」します。
実装
// app/dashboard/page.js (App RouterでのSSR対応版)
import { QueryClient, dehydrate } from '@tanstack/react-query';
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';
import Hydrate from '@/components/Hydrate'; // シンプルな水和コンポーネント
import DashboardClient from './DashboardClient'; // クライアントコンポーネント部分
// これはデータ取得関数で、サーバーから呼び出せる
const fetchOuraData = async (accessToken) => {
if (!accessToken) throw new Error('Not Authenticated');
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const dateStr = yesterday.toISOString().split('T')[0];
const ouraResponse = await fetch(
`https://api.ouraring.com/v2/usercollection/daily_activity?start_date=${dateStr}&end_date=${dateStr}`,
{
headers: { 'Authorization': `Bearer ${accessToken}` },
}
);
if (!ouraResponse.ok) throw new Error('Failed to fetch from Oura API');
return ouraResponse.json();
};
export default async function DashboardPage() {
const queryClient = new QueryClient();
const session = await getIronSession(cookies(), {
password: process.env.APP_SECRET,
cookieName: 'oura-session',
});
// サーバーでクエリをプリフェッチ
await queryClient.prefetchQuery({
queryKey: ['ouraDailySummary'],
queryFn: () => fetchOuraData(session.accessToken),
});
const dehydratedState = dehydrate(queryClient);
return (
<Hydrate state={dehydratedState}>
<DashboardClient />
</Hydrate>
);
}
// components/Hydrate.js
'use client';
import { Hydrate as RQHydrate } from '@tanstack/react-query';
function Hydrate(props) {
return <RQHydrate {...props} />;
}
export default Hydrate;
// app/dashboard/DashboardClient.js (以前のページロジックを含む)
'use client';
import { useQuery } from '@tanstack/react-query';
// バックグラウンド更新用のクライアントサイドフェッチャーが必要にりましたが、初期読み込み時には呼び出されません
const clientFetchOuraData = async () => {
const res = await fetch('/api/oura/daily-summary'); // プロキシを使用
if (!res.ok) {
throw new Error('Network response was not ok');
}
return res.json();
};
export default function DashboardClient() {
// このフックはクライアントでキャッシュ内のデータを即座に見つけます!
const { data, isLoading, isError, error } = useQuery({
queryKey: ['ouraDailySummary'],
queryFn: clientFetchOuraData,
});
// このUIロジックは同じまま
if (isLoading) return <span>Loading...</span>; // 最初の読み込みではこれは表示されません
if (isError) return <span>Error: {error.message}</span>;
const summary = data?.data?.[0];
if (!summary) return <span>昨日のデータがありません。</span>;
return (
<div>
<h1>{summary.day}のOuraデイリースマリー</h1>
<h2>読みどきスコア: {summary.score}</h2>
<ul>
<li>アクティビティスコア: {summary.contributors?.activity_balance}</li>
<li>睡眠スコア: {summary.contributors?.recovery_index}</li>
</ul>
</div>
);
}
仕組み
- サーバサイド(
DashboardPage):これは非同期Server Componentになりました。セッションを取得し、新しいQueryClientを作成し、queryClient.prefetchQueryを呼び出します。これはサーバーで直接データを取得します。 - 脱水:
dehydrate(queryClient)はクエリ結果とそのキャッシュメタデータをJSONにシリアライズします。 - 水和:この脱水状態を特殊な
<Hydrate>コンポーネントに渡します。 - クライアントサイド(
DashboardClient):ページがブラウザに読み込まれるとき、<QueryClientProvider>はサーバーで取得したデータで水和されます。DashboardClient内のuseQueryフックは['ouraDailySummary']のキャッシュにデータが存在することを確認し、瞬時に使用するため、初期レンダリング時の読み込み状態が回避されます。
まとめ
Next.jsアプリケーションでOura Ring APIとのフル機能、安全かつ高性能な統合を正常に構築しました。
成果のまとめ:
- ユーザーを安全に認証するための完全なサーバサイドOAuth 2.0フローを実装しました。
iron-sessionを使用して、機密性の高いアクセストークンを暗号化されたhttpOnlycookieに保存しました。- Oura APIへの全ての通信を処理する「プロキシ」APIルートを作成し、認証情報を安全に保ちました。
- 宣言的なデータ取得と強力な状態管理のためにReact Queryと
useQueryを活用しました。 - 最後に、React Queryの水和を使用してNext.jsサーバサイドレンダリングでデータをプリフェッチし、初期読み込み時間を短縮しSEOを向上させました。
この強力なパターンはOuraに限ったものではなく、任何のサードパーティAPI統合に適用でき、堅牢で心地よいユーザー体験を構築できます。
読者の次のステップ:
- より多くのデータを取得:睡眠期間やワークアウトなどの他のOura APIエンドポイントを探索します。
- 「ログアウト」ボタンを追加:セッションを破棄するAPIルートを作成します。
- トークンの更新を処理:Ouraアクセストークンは最終的に期限切れになります。最初のトークン交換で取得した
refresh_tokenを使用して自動的に新しいアクセストークンを取得するロジックを実装します。 - データを視覚化:Chart.jsやD3などのチャートライブラリを使用して、美しいヘルスダッシュボードを作成します。
参考資料
- 公式Oura API V2ドキュメント:Oura Cloud API Docs
- TanStack React Queryドキュメント:React Query SSR
- Next.jsドキュメント:Route Handlers
- GitHubのIron Session:iron-session
クイックリファレンス
| 項目 | 重要な情報 |
|---|---|
| トピック | 開発 |
| 関連性 | 患者教育と意識向上 |
| アクション | 医療提供者と相談してください |
| 頻度 | 加療チーム推奨通り |
| 根拠 | 現在の臨床ガイドラインに基づく |
よくある質問
Q: これが自分に該当するのかどうかはどうやってわかりますか? A: 個人的な病歴と家族歴について医療提供者と相談し、個々のリスク因子を評価し、適切な次のステップを決定してください。
Q: 医師に何を聞くべきですか? A: 良い質問には次のようなものがあります:「私の結果はどういう意味ですか?」「私 optionsは何ですか?」「どんな lifestyle changes would help?', and 'When should I follow up?'
Q: 健康状態を改善する可能性のある lifestyle changesはありますか? A: ほとんどの健康状態は基盤となる対策から benefits を受けます: balanced nutrition, regular physical activity (週150分以上), quality sleep (7〜9時間), stress management, and avoiding tobacco.
医療 DISCLAIMER
本文書は教育目的のみを目的としており、医療 advice を構成しません。健康、治療、またはケアに関する decisions を行う前は、常に資格のある医療提供者と相談してください。個人の状況は異なり、ある人 appropriate なものは別の 사람 には appropriate ではない可能性があります。
如果您正在经历医疗 emergency,请立即拨打 911(或您当地的 emergency number)。