12 KiB
12 KiB
Node.js Client Implementation Guide
This reference provides complete Node.js client implementations for the Asleep API, including webhook servers and production patterns.
Complete Node.js API Client
const axios = require('axios');
class AsleepClient {
constructor(apiKey, baseURL = 'https://api.asleep.ai') {
this.apiKey = apiKey;
this.baseURL = baseURL;
this.client = axios.create({
baseURL: baseURL,
headers: { 'x-api-key': apiKey }
});
}
async _request(method, path, options = {}) {
try {
const response = await this.client.request({
method,
url: path,
...options
});
return response.data;
} catch (error) {
if (error.response) {
const status = error.response.status;
const detail = error.response.data?.detail || 'Unknown error';
if (status === 401) {
throw new Error('Invalid API key');
} else if (status === 403) {
throw new Error(`API access error: ${detail}`);
} else if (status === 404) {
throw new Error('Resource not found');
}
}
throw error;
}
}
// User management
async createUser(metadata = null) {
const data = metadata ? { metadata } : {};
const result = await this._request('POST', '/ai/v1/users', { data });
return result.result.user_id;
}
async getUser(userId) {
const result = await this._request('GET', `/ai/v1/users/${userId}`);
return result.result;
}
async deleteUser(userId) {
await this._request('DELETE', `/ai/v1/users/${userId}`);
}
// Session management
async getSession(sessionId, userId, timezone = 'UTC') {
const result = await this._request('GET', `/data/v3/sessions/${sessionId}`, {
headers: { 'x-user-id': userId, 'timezone': timezone }
});
return result.result;
}
async listSessions(userId, options = {}) {
const { dateGte, dateLte, offset = 0, limit = 20, orderBy = 'DESC' } = options;
const params = { offset, limit, order_by: orderBy };
if (dateGte) params.date_gte = dateGte;
if (dateLte) params.date_lte = dateLte;
const result = await this._request('GET', '/data/v1/sessions', {
headers: { 'x-user-id': userId },
params
});
return result.result;
}
async deleteSession(sessionId, userId) {
await this._request('DELETE', `/ai/v1/sessions/${sessionId}`, {
headers: { 'x-user-id': userId }
});
}
// Statistics
async getAverageStats(userId, startDate, endDate, timezone = 'UTC') {
const result = await this._request('GET', `/data/v1/users/${userId}/average-stats`, {
headers: { 'timezone': timezone },
params: { start_date: startDate, end_date: endDate }
});
return result.result;
}
}
// Usage
const client = new AsleepClient(process.env.ASLEEP_API_KEY);
Express Webhook Server
const express = require('express');
const app = express();
app.use(express.json());
const EXPECTED_API_KEY = process.env.ASLEEP_API_KEY;
app.post('/asleep-webhook', async (req, res) => {
// Verify authentication
const apiKey = req.headers['x-api-key'];
const userId = req.headers['x-user-id'];
if (apiKey !== EXPECTED_API_KEY) {
console.warn('Unauthorized webhook attempt');
return res.status(401).json({ error: 'Unauthorized' });
}
const { event, session_id, stat } = req.body;
console.log(`Received ${event} event for user ${userId}`);
try {
switch (event) {
case 'INFERENCE_COMPLETE':
await handleInferenceComplete(req.body);
break;
case 'SESSION_COMPLETE':
await handleSessionComplete(req.body);
break;
default:
console.warn(`Unknown event type: ${event}`);
}
res.status(200).json({ status: 'success' });
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).json({ error: 'Processing failed' });
}
});
async function handleInferenceComplete(event) {
const { session_id, user_id, sleep_stages } = event;
// Update real-time dashboard
await updateLiveDashboard(session_id, sleep_stages);
// Store incremental data
await db.collection('incremental_data').insertOne(event);
console.log(`Processed INFERENCE_COMPLETE for session ${session_id}`);
}
async function handleSessionComplete(event) {
const { session_id, user_id, stat, session } = event;
// Store complete report
await db.collection('sleep_reports').insertOne({
user_id,
session_id,
date: session.start_time,
statistics: stat,
session_data: session,
created_at: new Date()
});
// Send user notification
await sendPushNotification(user_id, {
title: 'Sleep Report Ready',
body: `Sleep time: ${stat.sleep_time}, Efficiency: ${stat.sleep_efficiency.toFixed(1)}%`
});
// Update user statistics
await updateUserAggregatedStats(user_id);
console.log(`Processed SESSION_COMPLETE for session ${session_id}`);
}
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => {
console.log(`Webhook server listening on port ${PORT}`);
});
Retry with Exponential Backoff
async function retryWithExponentialBackoff(
func,
maxRetries = 3,
baseDelay = 1000,
maxDelay = 60000
) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await func();
} catch (error) {
if (error.response?.status === 403) {
const detail = error.response.data?.detail || '';
if (detail.toLowerCase().includes('rate limit')) {
if (attempt < maxRetries - 1) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
console.log(`Rate limited, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
}
}
throw error;
}
}
}
// Usage
const result = await retryWithExponentialBackoff(
() => client.getSession('session123', 'user123')
);
Basic Usage Examples
Creating Users
// Create user with metadata
const userId = await client.createUser({
birth_year: 1990,
gender: 'male',
height: 175.5,
weight: 70.0
});
console.log(`Created user: ${userId}`);
// Create user without metadata
const userId = await client.createUser();
Getting Sessions
// Get sessions for date range
const sessions = await client.listSessions('user123', {
dateGte: '2024-01-01',
dateLte: '2024-01-31',
limit: 50,
orderBy: 'DESC'
});
console.log(`Found ${sessions.sleep_session_list.length} sessions`);
sessions.sleep_session_list.forEach(session => {
console.log(`Session ${session.session_id}: ${session.session_start_time}`);
console.log(` State: ${session.state}, Time in bed: ${session.time_in_bed}s`);
});
Getting Session Details
const session = await client.getSession(
'session123',
'user123',
'America/New_York'
);
console.log(`Sleep efficiency: ${session.stat.sleep_efficiency.toFixed(1)}%`);
console.log(`Total sleep time: ${session.stat.sleep_time}`);
console.log(`Sleep stages: ${session.session.sleep_stages}`);
console.log(`Sleep cycles: ${session.stat.sleep_cycle.length}`);
Getting Statistics
const stats = await client.getAverageStats(
'user123',
'2024-01-01',
'2024-01-31',
'UTC'
);
const avg = stats.average_stats;
console.log(`Average sleep time: ${avg.sleep_time}`);
console.log(`Average efficiency: ${avg.sleep_efficiency.toFixed(1)}%`);
console.log(`Average bedtime: ${avg.start_time}`);
console.log(`Average wake time: ${avg.end_time}`);
console.log(`Light sleep ratio: ${(avg.light_ratio * 100).toFixed(1)}%`);
console.log(`Deep sleep ratio: ${(avg.deep_ratio * 100).toFixed(1)}%`);
console.log(`REM sleep ratio: ${(avg.rem_ratio * 100).toFixed(1)}%`);
console.log(`Number of sessions: ${stats.slept_sessions.length}`);
Asynchronous Webhook Processing
const Queue = require('bull');
const webhookQueue = new Queue('asleep-webhooks', {
redis: {
host: 'localhost',
port: 6379
}
});
app.post('/webhook', async (req, res) => {
const event = req.body;
// Queue for async processing
await webhookQueue.add(event);
// Respond immediately
res.status(200).json({ status: 'queued' });
});
// Process queued webhooks
webhookQueue.process(async (job) => {
const event = job.data;
if (event.event === 'SESSION_COMPLETE') {
await handleSessionComplete(event);
} else if (event.event === 'INFERENCE_COMPLETE') {
await handleInferenceComplete(event);
}
});
Idempotency Pattern
async function handleSessionComplete(event) {
const sessionId = event.session_id;
// Check if already processed
const existing = await db.collection('processed_webhooks').findOne({
session_id: sessionId,
event: 'SESSION_COMPLETE'
});
if (existing) {
console.log(`Session ${sessionId} already processed, skipping`);
return;
}
// Process event
await saveSleepReport(event);
// Mark as processed
await db.collection('processed_webhooks').insertOne({
session_id: sessionId,
event: 'SESSION_COMPLETE',
processed_at: new Date()
});
}
Comprehensive Error Handling
class AsleepAPIError extends Error {
constructor(message, statusCode, detail) {
super(message);
this.name = 'AsleepAPIError';
this.statusCode = statusCode;
this.detail = detail;
}
}
class RateLimitError extends AsleepAPIError {
constructor(detail) {
super('Rate limit exceeded', 403, detail);
this.name = 'RateLimitError';
}
}
class ResourceNotFoundError extends AsleepAPIError {
constructor(detail) {
super('Resource not found', 404, detail);
this.name = 'ResourceNotFoundError';
}
}
async function safeApiRequest(requestFunc) {
try {
return await requestFunc();
} catch (error) {
if (error.response) {
const status = error.response.status;
const detail = error.response.data?.detail || 'Unknown error';
if (status === 401) {
throw new AsleepAPIError('Authentication failed', 401, detail);
} else if (status === 403) {
if (detail.toLowerCase().includes('rate limit')) {
throw new RateLimitError(detail);
} else {
throw new AsleepAPIError('Access forbidden', 403, detail);
}
} else if (status === 404) {
throw new ResourceNotFoundError(detail);
} else {
throw new AsleepAPIError(`API error (${status})`, status, detail);
}
}
throw error;
}
}
// Usage
try {
const user = await safeApiRequest(() => client.getUser('user123'));
} catch (error) {
if (error instanceof ResourceNotFoundError) {
console.log('User not found, creating new user...');
const userId = await client.createUser();
} else if (error instanceof RateLimitError) {
console.log('Rate limited, try again later');
} else if (error instanceof AsleepAPIError) {
console.error(`API error: ${error.message}`);
}
}
Production Configuration
// config.js
require('dotenv').config();
class Config {
static get ASLEEP_API_KEY() {
return process.env.ASLEEP_API_KEY;
}
static get ASLEEP_BASE_URL() {
return process.env.ASLEEP_BASE_URL || 'https://api.asleep.ai';
}
static get DATABASE_URL() {
return process.env.DATABASE_URL;
}
static get REDIS_URL() {
return process.env.REDIS_URL;
}
static get WEBHOOK_SECRET() {
return process.env.WEBHOOK_SECRET;
}
static get ENABLE_CACHING() {
return process.env.ENABLE_CACHING !== 'false';
}
static validate() {
if (!this.ASLEEP_API_KEY) {
throw new Error('ASLEEP_API_KEY environment variable required');
}
if (!this.DATABASE_URL) {
throw new Error('DATABASE_URL environment variable required');
}
}
}
module.exports = Config;