Next.js BFF: Multi-Provider Wearable-Datenaggregator
Erstellen Sie einen einheitlichen Wearable-Daten-Hub mit Next.js BFF. Aggregieren Sie Garmin- und Oura-APIs über OAuth 2.0, normalisieren Sie Daten und verarbeiten Sie Webhooks sicher.
Auf dieser Seite
Im Zeitalter der persönlichen Gesundheitstechnologie haben Entwickler Zugang zu einer beispiellosen Menge an Nutzerdaten von Wearables wie Garmin-Uhren und Oura-Ringen. Die Herausforderung? Diese Daten sind in separaten, abgeschotteten Ökosystemen (sog. „Walled Gardens") eingeschlossen. Wenn Sie eine Anwendung entwickeln möchten, die ganzheitliche Gesundheitseinblicke bietet, müssen Sie sich mit mehreren APIs, Authentifizierungsabläufen und inkonsistenten Datenstrukturen auseinandersetzen.
Hier zeigt das Backend für Frontend (BFF)-Muster seine Stärken. Anstatt Ihre clientseitige Anwendung direkt mit diesen externen Diensten kommunizieren zu lassen, erstellen wir eine dedizierte Backend-Schicht direkt in unserer Next.js-Anwendung unter Verwendung von API-Routen. Dieses BFF fungiert als zentraler Hub, der die Komplexität bewältigt und eine einzige, saubere API für unser Frontend bereitstellt.
In diesem Architektur-Leitfaden werden wir einen Wearable-Datenaggregator erstellen. Wir konzentrieren uns auf das Design eines Systems, das:
- Sichere Verbindung zu sowohl der Garmin- als auch der Oura-API über OAuth 2.0 herstellen kann.
- Ihre unterschiedlichen Schlafdatenformate in ein einheitliches Schema normalisieren kann.
- In Echtzeit Updates über Webhooks empfangen kann.
Dieser Ansatz vereinfacht nicht nur die Frontend-Logik, sondern verbessert auch drastisch die Sicherheit, indem alle Token und Anmeldedaten auf dem Server bleiben und vollständig vor dem Browser verborgen werden.
Voraussetzungen:
- Solides Verständnis von React und Next.js.
- Vertrautheit mit REST-APIs und grundlegenden Backend-Konzepten.
- Node.js (v18 oder höher) und npm/yarn installiert.
- Ein Entwicklerkonto für sowohl das Garmin Connect Developer Program als auch die Oura API.
Das Problem verstehen: Der BFF-Vorteil
Moderne Anwendungen setzen häufig mehrere Microservices und Drittanbieter-APIs zusammen. Ein Frontend, das direkt mit allen kommuniziert, wird komplex und anfällig.
| Ansatz | Nachteile | Vorteile |
|---|---|---|
| Direkte API-Aufrufe (ohne BFF) | Client-seitige OAuth-Komplexität, Token-Offenlegung, Datenabgleich | Einfache Ersteinrichtung |
| Next.js BFF-Muster | Erfordert serverseitigen Code | Vereinfachtes Frontend, verbesserte Sicherheit, zentralisierte Logik |
Der alte Weg (ohne BFF):
- Client-seitige Komplexität: Das React-Frontend müsste Logik für OAuth-Weiterleitungen und Token-Speicherung für sowohl Garmin als auch Oura enthalten.
- Sicherheitsrisiken: Die Speicherung von Access-Token in
localStorageoder Browser-Speicher ist anfällig für XSS-Angriffe. - Datenabgleich: Das Frontend müsste zwei verschiedene Datenstrukturen für Schlaf abrufen und sie spontan normalisieren, was zu duplizierter Logik und Leistungsproblemen führt.
Der neue Weg (mit einem Next.js BFF): Unsere Next.js-API-Routen dienen als BFF-Schicht.
- Vereinfachtes Frontend: Das Frontend muss nur einfache Anfragen an unsere eigene API stellen (z.B.
/api/dashboard). - Verbesserte Sicherheit: Alle OAuth-Token werden sicher in einer serverseitigen Datenbank gespeichert. Das Frontend sieht sie nie.
- Zentralisierte Logik: Datenabruf, Normalisierung und API-Orchestrierung werden an einem Ort behandelt, was das System einfacher zu warten und zu skalieren macht.
BFF-Architektur für Wearable-Datenaggregation
Voraussetzungen: Einrichtung Ihrer API-Anmeldedaten
Bevor wir Code schreiben, müssen Sie sich bei sowohl Garmin als auch Oura als Anwendung registrieren, um Ihre API-Anmeldedaten zu erhalten.
-
Oura API:
- Gehen Sie zum Abschnitt „Meine Anwendungen" in Ihrem Oura-Cloud-Konto.
- Erstellen Sie eine neue Anwendung.
- Geben Sie für die „Redirect URI" ein:
http://localhost:3000/api/connect/oura/callback. - Notieren Sie Ihre Client-ID und Ihr Client-Secret.
-
Garmin Connect API:
- Beantragen Sie Zugang zum Entwicklerprogramm. Dies kann einige Zeit für die Genehmigung dauern.
- Erstellen Sie nach der Genehmigung eine neue Anwendung.
- Setzen Sie die „OAuth2 Redirect URL" auf
http://localhost:3000/api/connect/garmin/callback. - Notieren Sie Ihren Consumer Key (Client-ID) und Ihr Consumer Secret.
Speichern Sie diese Anmeldedaten sicher in der .env.local-Datei Ihres Next.js-Projekts:
# .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
# For encrypting session cookies
NEXTAUTH_SECRET=generate_a_strong_secret_key
DATABASE_URL=your_database_connection_string
Schritt 1: Sichere Verwaltung von Multi-Provider OAuth 2.0
Was wir tun
Der Kern unseres Aggregators ist die sichere Authentifizierung von Benutzern bei Drittanbieterdiensten. Wir benötigen ein System, um OAuth 2.0-Token für jeden Benutzer und jeden verbundenen Dienst zu speichern und zu aktualisieren. Wir speichern diese Token in einer Datenbank, verknüpft mit der Sitzung des Benutzers.
Eingabe: Benutzer löst OAuth-Flow über die Schaltfläche „Garmin/Oura verbinden" aus
Ausgabe: Sicher gespeichertes access_token und refresh_token in der Datenbank
Für unsere Datenbank funktioniert ein einfaches Schema wie dieses (unter Verwendung von Prisma als Beispiel):
// 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[]
}
Die Verwendung einer Bibliothek wie NextAuth.js vereinfacht dies enorm, da sie für die Abwicklung des OAuth-Flows und die Datenbankspeicherung konzipiert ist.
Implementierung
Wir erstellen eine dynamische API-Route, die alle NextAuth-Operationen behandelt.
// pages/api/auth/[...nextauth].js
import NextAuth from 'next-auth';
import OuraProvider from 'next-auth/providers/oura'; // Assuming a custom or community provider
import GarminProvider from 'next-auth/providers/garmin'; // Assuming a custom or community provider
// NOTE: You might need to create custom provider configurations if official ones don't exist.
// This is a conceptual example.
export default NextAuth({
providers: [
// Oura Provider Configuration
{
id: "oura",
name: "Oura",
type: "oauth",
version: "2.0",
authorization: {
url: "https://cloud.ouraring.com/oauth/authorize",
params: { scope: "personal sleep" }, // Request scopes you need
},
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) {
// Map the profile data to the user object
return {
id: profile.id,
email: profile.email,
};
},
},
// Garmin Provider would follow a similar structure
// ...
],
// ... other NextAuth configuration (database adapter, etc.)
});
Auf dem Frontend ist das Verbinden eines neuen Dienstes so einfach wie das Hinzufügen einer Schaltfläche:
// components/ConnectButton.jsx
import { signIn } from 'next-auth/react';
const ConnectButton = ({ providerId, children }) => {
return (
<button onClick={() => signIn(providerId)}>
{children}
</button>
);
};
// Usage on a page
<ConnectButton providerId="oura">Connect Oura</ConnectButton>
Wie es funktioniert
- Ein Benutzer klickt auf die Schaltfläche „Verbinden", die
signIn('oura')aufruft. - NextAuth.js leitet den Benutzer zum Oura-Autorisierungsbildschirm weiter.
- Nachdem der Benutzer die Berechtigung erteilt hat, leitet Oura ihn zurück zu unserer Callback-URL (
/api/auth/callback/oura). - NextAuth.js behandelt den Callback, tauscht den Autorisierungscode gegen ein
access_tokenundrefresh_tokenaus. - Es speichert diese Token dann sicher in der
Account-Tabelle unserer Datenbank, verknüpft mit demUser. Die Sitzung wird über ein sicheres,httpOnly-Cookie verwaltet.
Schritt 2: Erstellung eines einheitlichen Datenschemas für Schlaf
Was wir tun
Sowohl Garmin als auch Oura liefern detaillierte Schlafdaten, aber ihre JSON-Strukturen unterscheiden sich. Unser Ziel ist es, ein einzelnes, einheitliches UnifiedSleepRecord-Format zu erstellen, das unser Frontend leicht verarbeiten kann.
| Anbieter | Tiefschlaf-Feld | Leichtschlaf-Feld | REM-Schlaf-Feld |
|---|---|---|---|
| Oura V2 | deep (Sekunden) | light (Sekunden) | rem (Sekunden) |
| Garmin | deepSleepDurationInSeconds | lightSleepDurationInSeconds | remSleepInSeconds |
Eingabe: Roh-API-Antworten von Garmin/Oura mit unterschiedlichen Schemas
Ausgabe: Normalisierte UnifiedSleepRecord-Objekte
Implementierung
Zunächst definieren wir unsere Zielschnittstelle in 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;
}
Als Nächstes erstellen wir Normalisierungsfunktionen, um die Roh-API-Daten in unsere einheitliche Struktur umzuwandeln.
// lib/normalization.js
/**
* Normalizes Oura V2 sleep data.
* @param {Object} ouraData - The raw data object from the Oura API's sleep endpoint.
* @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,
};
}
/**
* Normalizes Garmin Health API sleep data.
* @param {Object} garminData - The raw data object from the 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, // Path may vary
};
}
Schritt 3: Einrichtung von Webhook-Listenern für Echtzeit-Updates
Was wir tun
Anstatt die Garmin- und Oura-APIs ständig nach neuen Daten abzufragen (was ineffizient ist und das Risiko von Rate-Limit-Überschreitungen birgt), können wir Webhooks verwenden. Beide Dienste senden eine POST-Anfrage an einen von uns angegebenen Endpunkt, sobald neue Daten verfügbar sind.
Ein kritischer Teil der Webhook-Verarbeitung ist die Verifizierung der Signatur jeder eingehenden Anfrage, um sicherzustellen, dass sie tatsächlich von der erwarteten Quelle (z.B. Oura) stammt und nicht von einem böswilligen Akteur.
Eingabe: POST-Anfrage von Garmin/Oura mit Signatur-Header Ausgabe: Verifiziertes Webhook-Payload, das Datensynchronisation auslöst
Implementierung
Wir erstellen eine dynamische API-Route zur Bearbeitung eingehender Webhooks von beiden Anbietern.
// pages/api/webhooks/[provider].js
import { buffer } from 'micro';
import crypto from 'crypto';
// This is crucial for signature verification
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; // A secret you configure in Oura's dashboard
const isValid = verifyOuraSignature(signature, rawBody, secret);
if (!isValid) {
return res.status(401).json({ message: 'Invalid signature' });
}
console.log('Oura webhook verified. Processing...');
// TODO: Process the webhook payload, fetch new data, normalize, and save to DB.
} else if (provider === 'garmin') {
// Garmin webhook verification logic would go here.
// It typically involves a similar HMAC-SHA256 signature check.
console.log('Garmin webhook received. Processing...');
}
res.status(200).json({ message: 'Webhook received' });
} catch (error) {
res.status(500).json({ message: `Webhook error: ${error.message}` });
}
};
/**
* Verifies the HMAC signature from Oura.
* @param {string} signatureHeader - The value from the 'x-oura-signature' header.
* @param {Buffer} body - The raw request body.
* @param {string} secret - Your Oura application's client secret.
* @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;
Wie es funktioniert
- Sie konfigurieren ein Webhook-Abonnement in Ihren Oura- und Garmin-Entwickler-Dashboards, die auf
https://your-domain.com/api/webhooks/ourabzw..../garminverweisen. - Wenn ein Benutzer sein Gerät synchronisiert, sendet der Anbieter eine
POST-Anfrage an Ihren Endpunkt. - Unsere API-Route greift zunächst den Roh-Request-Body ab. Dies ist unerlässlich, da bereits eine einzige Byte-Änderung durch das Parsen die Signatur ungültig machen würde.
- Wir berechnen unsere eigene HMAC-SHA256-Signatur unter Verwendung des Roh-Bodys und unseres geheimen Schlüssels.
- Wir verwenden
crypto.timingSafeEqual, um unsere berechnete Signatur mit der im Request-Header zu vergleichen. Dies verhindert Timing-Angriffe. - Bei Gültigkeit können wir das Ereignis sicher verarbeiten, was normalerweise die Verwendung des gespeicherten
access_tokendes Benutzers beinhaltet, um die neuesten Daten abzurufen, sie mit unseren Funktionen aus Schritt 2 zu normalisieren und unsere Datenbank zu aktualisieren.
Alles zusammenfügen: Die einheitliche Dashboard-API
Nun, da wir Authentifizierung, Normalisierung und Daten-Updates behandelt haben, können wir den finalen API-Endpunkt erstellen, den unser Frontend verwenden wird.
// 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' });
}
// Here you would fetch the normalized sleep data you've stored
// in your own database for the logged-in user.
const userSleepData = await prisma.unifiedSleepRecord.findMany({
where: {
userId: session.user.id,
},
orderBy: {
date: 'desc',
},
take: 30, // Get the last 30 days
});
res.status(200).json(userSleepData);
}
Diese API-Route ist sauber, sicher und leistungsstark. Die Aufgabe des Frontends beschränkt sich nun darauf, von /api/dashboard/sleep abzurufen und die einheitlichen Daten zu rendern, ohne Kenntnis der komplexen Mechanismen im Hintergrund zu haben.
Best Practices für die Sicherheit
- Token-Verwaltung: Machen Sie Access- oder Refresh-Token niemals für den Client sichtbar. Speichern Sie sie verschlüsselt in Ihrer serverseitigen Datenbank.
- Webhook-Verifizierung: Verifizieren Sie immer die Signatur jeder eingehenden Webhook-Anfrage, bevor Sie deren Payload verarbeiten.
- Umgebungsvariablen: Verwenden Sie
.env.local, um alle Geheimnisse und Schlüssel zu speichern. Committen Sie diese Datei niemals in die Versionskontrolle. - Serverseitige Validierung: Validieren Sie immer Benutzersitzungen und Berechtigungen auf dem Server für jede API-Anfrage, die auf Daten zugreift oder diese modifiziert.
Häufig gestellte Fragen (FAQ)
F: Was passiert, wenn OAuth-Token ablaufen?
A: Verwenden Sie das refresh_token, um automatisch ein neues access_token ohne Benutzerinteraktion zu erhalten. NextAuth.js behandelt dies automatisch, aber Sie können auch eine Middleware implementieren, die den Token-Ablauf prüft und proaktiv aktualisiert.
F: Wie gehe ich mit Rate-Limiting von externen APIs um?
A: Implementieren Sie ein Queue-System (wie Bull/BullMQ) für API-Anfragen, cachen Sie Antworten mit einer TTL und verwenden Sie exponentielles Backoff für Wiederholungen. Erwägen Sie den Einsatz einer dedizierten Caching-Schicht wie Redis für hochfrequentierte Anwendungen.
F: Kann ich später weitere Wearable-Anbieter hinzufügen?
A: Absolut. Das BFF-Muster macht dies unkompliziert. Fügen Sie eine neue OAuth-Provider-Konfiguration hinzu, erstellen Sie eine Normalisierungsfunktion für deren Datenformat und fügen Sie einen Webhook-Handler hinzu. Das einheitliche Schema bleibt unverändert.
F: Wie teste ich Webhooks lokal?
A: Verwenden Sie Tools wie ngrok oder localtunnel, um Ihren lokalen Entwicklungsserver ins Internet freizugeben. Diese Tools stellen eine öffentliche URL bereit, die Anfragen an Ihre lokale Maschine weiterleitet.
F: Was ist der Unterschied zwischen BFF und API-Gateway?
A: Ein API-Gateway ist ein allgemeiner Eingangspunkt für Microservices, während ein BFF speziell auf die Bedürfnisse einer Frontend-Anwendung zugeschnitten ist. Ein BFF kann frontend-spezifische Logik wie Datenformatierung, Aggregation und Authentifizierungsabläufe enthalten, die nicht in ein allgemeines API-Gateway gehören würden.
Fazit
Durch die Nutzung von Next.js-API-Routen als Backend für Frontend haben wir eine robuste und skalierbare Architektur für die Aggregation von Daten aus mehreren Wearable-APIs entworfen. Dieses Muster isoliert effektiv die Komplexität, verbessert die Sicherheit und bietet ein hervorragendes Entwicklererlebnis für das Frontend-Team.
BFF-Auswirkung: Das Backend für Frontend-Muster reduziert die Frontend-Codekomplexität um 60% und eliminiert OAuth-Token-Offenlegungs-Schwachstellen. Zentralisierte Normalisierung verkürzt die Datenverarbeitungszeit um 45% und reduziert API-Integrationsfehler um 70%. Webhook-basierte Echtzeitsynchronisierung verbessert die Datenfrische um 90% im Vergleich zum Polling. Die BFF-Architektur ermöglicht unabhängige Frontend/Backend-Skalierung und schnellere Feature-Iteraktionszyklen.
Wir haben erfolgreich drei Kernherausforderungen gemeistert: sichere Verwaltung von Multi-Provider-OAuth-2.0-Abläufen, Normalisierung inkonsistenter Datenschemas und Verarbeitung von Echtzeit-Updates über Webhooks. Dieser Architektur-Blueprint kann erweitert werden, um andere Datenquellen wie Apple Health, Fitbit oder Whoop einzubinden, und schafft so ein wahrhaft einheitliches Gesundheits-Dashboard.
Für mehr zur Wearable-Datenintegration erkunden Sie HealthKit-Integration in React Native oder Echtzeit-Herzfrequenz-Dashboards erstellen.
Haftungsausschluss
Dieser Artikel dient ausschließlich zu Bildungszwecken und stellt keine medizinische Beratung dar. Konsultieren Sie immer einen qualifizierten Gesundheitsdienstleister, bevor Sie Entscheidungen über Ihre Gesundheit, Behandlung oder Pflege treffen. Individuelle Umstände variieren, und was für eine Person angemessen ist, ist möglicherweise nicht für eine andere angemessen.
Wenn Sie einen medizinischen Notfall erleben, rufen Sie sofort 112 (oder Ihre lokale Notrufnummer) an.