Serverseitige Oura-Ring-Daten in React: Next.js-Tutorial
Erfahren Sie, wie Sie die Oura-Ring-API mit Next.js und React Query integrieren. Das Tutorial behandelt OAuth 2.0, serverseitigen Datenabruf und SSR.
Auf dieser Seite
Einstieg: Health Tech boomt, und Entwicklerinnen und Entwickler stehen an vorderster Front und bauen personalisierte Wellness-Anwendungen. Wearables wie der Oura Ring liefern einen wahren Datenschatz – Schlaf, Readiness und Aktivität –, der diese Erlebnisse antreiben kann. Diese Daten jedoch sicher und effizient in eine Webanwendung zu integrieren, kann eine komplexe Herausforderung sein, die Authentifizierung, Server-State-Management und Performance-Optimierung umfasst.
Was wir bauen/lernen: In diesem Tutorial bauen wir eine Next.js-Anwendung, die sich über OAuth 2.0 sicher mit der Oura-Ring-API verbindet. Anschließend nutzen wir die Möglichkeiten von TanStacks React Query, um die Schlaf-, Readiness- und Aktivitätswerte einer Nutzerin oder eines Nutzers abzurufen, zwischenzuspeichern und anzuzeigen. Wir konzentrieren uns auf einen Ansatz mit serverseitigem Rendering (SSR), damit unsere Daten aktuell und sicher sind und geladen werden, bevor die Seite überhaupt an den Client gesendet wird.
Voraussetzungen:
- Node.js (v18 oder neuer)
- Grundlegendes Verständnis von React und Next.js
- Ein Oura Ring und ein aktives Oura-Konto
- Vertrautheit mit async/await in JavaScript
Warum das für Entwicklerinnen und Entwickler wichtig ist: Dieser Leitfaden behandelt ein Problem aus der Praxis: den sicheren Umgang mit Daten von Drittanbieter-APIs in einem modernen Web-Framework. Sie lernen ein robustes Muster, um API-Schlüssel und Zugriffstoken auf dem Server zu verwalten und eine Offenlegung gegenüber dem Client zu verhindern. Wenn Sie React Query mit SSR beherrschen, können Sie leistungsstarke Anwendungen bauen, die sich für die Nutzerinnen und Nutzer unglaublich reaktionsschnell anfühlen.
Das Problem verstehen
Die Integration einer Drittanbieter-API wie der von Oura bringt mehrere Herausforderungen mit sich:
- Authentifizierung: Die Oura-API verwendet OAuth 2.0, ein leistungsfähiges, aber mehrstufiges Authentifizierungsprotokoll. Wir benötigen einen sicheren, serverseitigen Ablauf, um den Austausch eines temporären Codes gegen ein langlebiges Zugriffstoken zu handhaben.
- Sicherheit: Das Client Secret und die Zugriffstoken der Nutzer sind hochsensibel. Sie dürfen niemals auf der Client-Seite offengelegt werden. Alle API-Aufrufe an Oura müssen aus einer sicheren Serverumgebung stammen.
- Datenabruf und Zustandsverwaltung: Das Abrufen von Daten bringt Boilerplate-Code mit sich. Wir müssen Ladezustände, Fehlerzustände und Caching verwalten, um überflüssige API-Aufrufe zu vermeiden. Das manuell mit
useStateunduseEffectzu tun, kann schnell unübersichtlich werden. - Nutzererlebnis: Wir möchten Ladeindikatoren möglichst vermeiden. Die Daten sollten vorhanden sein, wenn die Seite geladen wird – ein idealer Anwendungsfall für die serverseitigen Rendering-Fähigkeiten von Next.js.
Unser Ansatz – Next.js-Route-Handler für den OAuth-Ablauf und das API-Proxying, kombiniert mit React Query für Zustandsverwaltung und SSR – löst diese Herausforderungen elegant und effizient.
Voraussetzungen
Bevor wir mit dem Programmieren beginnen, richten wir unsere Oura-Anwendung ein.
-
Ein Oura-Konto erstellen: Falls Sie noch keines haben, registrieren Sie sich unter cloud.ouraring.com.
-
Eine OAuth-Anwendung registrieren:
- Navigieren Sie zum Bereich API Applications.
- Klicken Sie auf „Create New Application“.
- Füllen Sie die Angaben aus. Tragen Sie bei „Redirect URIs“
http://localhost:3000/api/auth/callback/ouraein. Dieser Schritt ist für den OAuth-Ablauf entscheidend. - Nach der Erstellung erhalten Sie eine Client ID und ein Client Secret. Bewahren Sie diese sicher auf!
-
Umgebungsvariablen einrichten:
- Erstellen Sie im Stammverzeichnis Ihres Next.js-Projekts eine Datei namens
.env.local. - Fügen Sie Ihre Oura-Zugangsdaten und ein Geheimnis zum Verschlüsseln der Sitzungsdaten hinzu.
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'Hinweis: Wir legen die Client ID öffentlich offen (
NEXT_PUBLIC_), weil sie auf dem Client benötigt wird, um den Anmeldevorgang zu starten. Das Client Secret bleibt sicher auf dem Server. - Erstellen Sie im Stammverzeichnis Ihres Next.js-Projekts eine Datei namens
-
Eine neue Next.js-App erstellen und Abhängigkeiten installieren:
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: Die Kernbibliothek für die Verwaltung des Serverzustands.@tanstack/react-query-devtools: Ein hilfreiches Werkzeug zum Debuggen des Caches von React Query.iron-session: Eine Bibliothek zum Erstellen sicherer, verschlüsselter HTTPOnly-Cookies zum Speichern unserer Sitzungsdaten (z. B. des Oura-Zugriffstokens).
Schritt 1: Den OAuth-2.0-Ablauf implementieren
Unsere erste große Aufgabe ist es, die Nutzerin oder den Nutzer bei Oura zu authentifizieren. Das umfasst drei Teile: die Weiterleitung zu Oura, die Verarbeitung des Callbacks und das sichere Speichern des Zugriffstokens.
Was wir tun
Wir erstellen zwei API-Routen in Next.js:
/api/auth/login/oura: Diese Route erstellt die Autorisierungs-URL von Oura und leitet die Nutzerin oder den Nutzer dorthin weiter./api/auth/callback/oura: Oura leitet nach der Zustimmung zu unserer App hierher zurück. Diese Route tauscht den erhaltenencodegegen einaccess_tokenund speichert es in einem verschlüsselten Cookie.
Implementierung
Zuerst richten wir unser Hilfsmodul für die Sitzungsverwaltung ein.
// 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);
}
Nun zur Login-Route. Diese Route leitet die Nutzerin oder den Nutzer einfach zum Einwilligungsbildschirm von Oura weiter.
// 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'); // Tägliche Schlaf-, Aktivitäts- und Readiness-Daten anfordern.
return NextResponse.redirect(authUrl.toString());
}
Schließlich die Callback-Route. Hier geschieht das Wesentliche.
// 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) {
// Fehler behandeln: Es wurde kein Code übergeben
return NextResponse.redirect(new URL('/?error=oauth_error', request.url));
}
// Code gegen Zugriffstoken tauschen
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) {
// Fehler behandeln: Token konnte nicht abgerufen werden
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();
// Token verschlüsseln und in einem Cookie speichern
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();
// Zur Dashboard-Seite weiterleiten
return NextResponse.redirect(new URL('/dashboard', request.url));
}
So funktioniert es
Der OAuth-2.0-Ablauf „Authorization Code“ ist ein Standard für sichere Authentifizierung.
- Unsere App leitet die Nutzerin oder den Nutzer zum Autorisierungsserver von Oura weiter.
- Die Person meldet sich bei Oura an und erteilt unserer Anwendung die Berechtigung, auf ihre
daily-Daten zuzugreifen. - Oura leitet zur angegebenen
redirect_uri(/api/auth/callback/oura) zurück und übergibt einen temporärencode. - Unser serverseitiger Callback-Handler empfängt diesen
code. Anschließend sendet er eine sichere Server-zu-Server-POST-Anfrage an den Endpunkt/oauth/tokenvon Oura, einschließlich unseresclient_secret. - Oura prüft die Anfrage und gibt ein
access_tokenzurück. - Wir speichern dieses Token mit
iron-sessionin einem verschlüsseltenhttpOnly-Cookie. Das Token ist damit sicher gespeichert und kann von clientseitigem JavaScript nicht gelesen werden, was XSS-Angriffe eindämmt.
Schritt 2: Oura-Daten mit React Query abrufen
Da wir nun ein Zugriffstoken haben, können wir die Daten der Nutzerin oder des Nutzers abrufen. Wir erstellen eine weitere API-Route als Proxy zur Oura-API und rufen sie im Frontend mit React Query auf.
Was wir tun
- Eine API-Route
/api/oura/daily-summaryerstellen, die das Zugriffstoken aus dem Sitzungs-Cookie liest und Daten von der offiziellen Oura-API abruft. - Einen React-Query-
QueryClientProviderin unserer_app.jsoderlayout.jseinrichten. - Den Hook
useQueryauf unserer Dashboard-Seite verwenden, um unsere interne API-Route aufzurufen und die Daten anzuzeigen.
Implementierung
Zuerst der serverseitige API-Proxy. Diese Route schützt unser Zugriffstoken.
// 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 });
}
// Datum von gestern im Format JJJJ-MM-TT ermitteln
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);
}
Als Nächstes richten wir den React-Query-Provider ein. Dadurch steht der Query-Client in der gesamten App zur Verfügung.
// 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>
);
}
Zum Schluss bauen wir die Dashboard-Seite, die mit useQuery die Daten abruft und anzeigt.
// 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>Wird geladen...</span>;
}
if (isError) {
return <span>Fehler: {error.message}</span>;
}
// Es wird angenommen, dass die API Daten im erwarteten Format zurückgibt
const summary = data?.data?.;
if (!summary) {
return <span>Für gestern sind keine Daten verfügbar.</span>
}
return (
<div>
<h1>Ihre tägliche Oura-Zusammenfassung für {summary.day}</h1>
<h2>Readiness-Wert: {summary.score}</h2>
<ul>
<li>Aktivitätswert: {summary.contributors?.activity_balance}</li>
<li>Schlafwert: {summary.contributors?.recovery_index}</li>
</ul>
</div>
);
}
So funktioniert es
useQuery ist das Kernstück von React Query. Es erhält einen eindeutigen queryKey und eine queryFn (eine asynchrone Funktion, die unsere Daten zurückgibt). React Query übernimmt automatisch:
- Caching: Wenn wir die Seite verlassen und wieder aufrufen, werden die Daten sofort aus dem Cache geliefert, während im Hintergrund ein neuer Abruf erfolgt.
- Lade- und Fehlerzustände: Die booleschen Werte
isLoadingundisErrorvereinfachen unsere UI-Logik. - Aktualisierung im Hintergrund: Die Daten können automatisch neu abgerufen werden, wenn das Fenster wieder den Fokus erhält, sodass die Oberfläche aktuell bleibt.
Alles zusammenführen mit serverseitigem Rendering (SSR)
Der clientseitige Abruf funktioniert, aber die Nutzerin oder der Nutzer sieht die Meldung „Wird geladen...“. Das geht besser. Mit getServerSideProps von Next.js und dem Prefetching-Mechanismus von React Query können wir die Daten auf dem Server abrufen und das vollständige HTML rendern.
Was wir tun
Wir überarbeiten unsere Dashboard-Seite so, dass eine Server Component die Daten zunächst abruft. React Query unterstützt dies über einen Mechanismus namens „Hydration“. Wir rufen die Daten auf dem Server vorab ab, „dehydrieren“ den Cache (wandeln ihn in ein serialisierbares Format um) und „rehydrieren“ ihn anschließend auf dem Client.
Implementierung
// app/dashboard/page.js (Aktualisiert für SSR mit dem App Router)
import { QueryClient, dehydrate } from '@tanstack/react-query';
import { getIronSession } from 'iron-session';
import { cookies } from 'next/headers';
import Hydrate from '@/components/Hydrate'; // Eine einfache Hydration-Komponente
import DashboardClient from './DashboardClient'; // Der clientseitige Komponententeil
// Dies ist unsere Datenabruffunktion, nun auf dem Server aufrufbar
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');
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',
});
// Wir rufen die Query auf dem Server vorab ab
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 (Enthält unsere bisherige Seitenlogik)
'use client';
import { useQuery } from '@tanstack/react-query';
// Wir benötigen nun einen clientseitigen Fetcher für Aktualisierungen im Hintergrund, er wird aber beim ersten Laden nicht aufgerufen.
const clientFetchOuraData = async () => {
const res = await fetch('/api/oura/daily-summary'); // verwendet unseren Proxy
if (!res.ok) {
throw new Error('Network response was not ok');
}
return res.json();
};
export default function DashboardClient() {
// Dieser Hook findet die Daten auf dem Client sofort im Cache!
const { data, isLoading, isError, error } = useQuery({
queryKey: ['ouraDailySummary'],
queryFn: clientFetchOuraData,
});
// Diese UI-Logik bleibt gleich
if (isLoading) return <span>Wird geladen...</span>; // Beim ersten Laden sehen Sie dies nicht
if (isError) return <span>Fehler: {error.message}</span>;
const summary = data?.data?.;
if (!summary) return <span>Für gestern sind keine Daten verfügbar.</span>;
return (
<div>
<h1>Ihre tägliche Oura-Zusammenfassung für {summary.day}</h1>
<h2>Readiness-Wert: {summary.score}</h2>
<ul>
<li>Aktivitätswert: {summary.contributors?.activity_balance}</li>
<li>Schlafwert: {summary.contributors?.recovery_index}</li>
</ul>
</div>
);
}
So funktioniert es
- Serverseite (
DashboardPage): Dies ist nun eine asynchrone Server Component. Sie liest die Sitzung, erstellt einen neuenQueryClientund ruftqueryClient.prefetchQueryauf. Dadurch werden die Daten direkt auf dem Server abgerufen. - Dehydration:
dehydrate(queryClient)serialisiert das Query-Ergebnis und seine Cache-Metadaten in JSON. - Hydration: Wir übergeben diesen dehydrierten Zustand an eine spezielle Komponente
<Hydrate>. - Clientseite (
DashboardClient): Wenn die Seite im Browser geladen wird, wird der<QueryClientProvider>mit diesen serverseitig abgerufenen Daten rehydriert. Der HookuseQueryinDashboardClienterkennt, dass Daten für['ouraDailySummary']bereits im Cache vorhanden sind, und verwendet sie sofort, sodass beim ersten Rendern kein Ladezustand auftritt.
Fazit
Wir haben erfolgreich eine vollständige, sichere und performante Integration der Oura-Ring-API in einer Next.js-Anwendung umgesetzt.
Zusammenfassung der Ergebnisse:
- Wir haben den vollständigen serverseitigen OAuth-2.0-Ablauf implementiert, um Nutzerinnen und Nutzer sicher zu authentifizieren.
- Wir haben
iron-sessionverwendet, um sensible Zugriffstoken in verschlüsseltenhttpOnly-Cookies zu speichern. - Wir haben eine „Proxy“-API-Route erstellt, die die gesamte Kommunikation mit der Oura-API übernimmt und unsere Zugangsdaten schützt.
- Wir haben React Query und
useQueryfür deklarativen Datenabruf und eine leistungsfähige Zustandsverwaltung genutzt. - Schließlich haben wir das serverseitige Rendering von Next.js mit der Hydration von React Query verwendet, um Daten vorab abzurufen, die anfängliche Ladezeit zu beseitigen und die SEO zu verbessern.
Dieses leistungsstarke Muster ist nicht auf Oura beschränkt; Sie können es auf jede Integration einer Drittanbieter-API anwenden und robuste, angenehme Nutzererlebnisse schaffen.
Nächste Schritte für die Leserinnen und Leser:
- Weitere Daten abrufen: Erkunden Sie andere Endpunkte der Oura-API, etwa Schlafphasen oder Workouts.
- Eine Schaltfläche „Abmelden“ hinzufügen: Erstellen Sie eine API-Route, die die Sitzung beendet.
- Token-Aktualisierung behandeln: Das Oura-Zugriffstoken läuft irgendwann ab. Implementieren Sie eine Logik, die das
refresh_token(aus dem ersten Token-Austausch) verwendet, um automatisch ein neues Zugriffstoken zu erhalten. - Die Daten visualisieren: Nutzen Sie eine Diagrammbibliothek wie Chart.js oder D3, um ein ansprechendes Gesundheits-Dashboard zu erstellen.
Ressourcen
- Offizielle Dokumentation der Oura API V2: Oura Cloud API Docs
- Dokumentation von TanStack React Query: React Query SSR
- Next.js-Dokumentation: Route Handlers
- Iron Session auf GitHub: iron-session
Kurzreferenz
| Aspekt | Wesentliche Information |
|---|---|
| Thema | Entwicklung |
| Relevanz | Patientenaufklärung und Bewusstsein |
| Maßnahme | Besprechen Sie dies mit Ihrer Ärztin oder Ihrem Arzt |
| Häufigkeit | Wie von Ihrem Behandlungsteam empfohlen |
| Evidenz | Basierend auf aktuellen klinischen Leitlinien |
Häufig gestellte Fragen
F: Woran erkenne ich, ob das auf mich zutrifft? A: Besprechen Sie Ihre persönliche und familiäre Krankengeschichte mit Ihrer Ärztin oder Ihrem Arzt, um Ihre individuellen Risikofaktoren einzuschätzen und die geeigneten nächsten Schritte festzulegen.
F: Was sollte ich meine Ärztin oder meinen Arzt fragen? A: Gute Fragen sind: „Was bedeuten meine Ergebnisse?“, „Welche Möglichkeiten habe ich?“, „Welche Änderungen des Lebensstils würden helfen?“ und „Wann sollte ich zur Nachkontrolle kommen?“
F: Gibt es Änderungen des Lebensstils, die helfen können? A: Den meisten Erkrankungen kommen grundlegende Maßnahmen zugute: ausgewogene Ernährung, regelmäßige körperliche Aktivität (150+ Minuten pro Woche), erholsamer Schlaf (7–9 Stunden), Stressbewältigung und der Verzicht auf Tabak.
Medizinischer Haftungsausschluss
Dieser Artikel dient ausschließlich der Aufklärung und stellt keine medizinische Beratung dar. Konsultieren Sie stets eine qualifizierte medizinische Fachperson, bevor Sie Entscheidungen über Ihre Gesundheit, Behandlung oder Versorgung treffen. Die individuellen Umstände unterscheiden sich, und was für eine Person angemessen ist, muss für eine andere nicht angemessen sein.
Wenn Sie einen medizinischen Notfall erleben, rufen Sie sofort 911 (oder Ihre örtliche Notrufnummer) an.