Production Integration Patterns
This guide shows production-ready patterns for integrating the Fluency API. All examples assume you have an OpenAPI-generated client from the API Reference.
Session State Management
Track session state across questions and handle pause/resume:
interface SessionState {
sessionId: string;
skillId: string;
algorithmId: 'speedrun' | 'practice';
activeSessionTime: number; // seconds
lives: number;
isPaused: boolean;
questionCount: number;
}
// Initialize
const session: SessionState = {
sessionId: crypto.randomUUID(),
skillId: selectedSkill.id,
algorithmId: 'practice',
activeSessionTime: 0,
lives: 3,
isPaused: false,
questionCount: 0,
};
// Pause tracking
function pauseSession() {
session.isPaused = true;
stopTimer();
}
// Resume with same sessionId
function resumeSession() {
session.isPaused = false;
startTimer();
}
Session Persistence
For apps that may close mid-session:
// Save after each answer
localStorage.setItem('current_session', JSON.stringify(session));
// Restore on reopen
const saved = localStorage.getItem('current_session');
if (saved) {
const state = JSON.parse(saved);
// Validate not stale (< 1 hour old)
const age = Date.now() - state.startedAt;
if (age < 60 * 60 * 1000) {
session = state; // Resume
}
}
Error Handling
Network Errors with Retry
async function fetchWithRetry<T>(fetchFn: () => Promise<T>, maxRetries = 3): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fetchFn();
} catch (error) {
const isLastAttempt = attempt === maxRetries - 1;
// Don't retry client errors (4xx)
if (error.statusCode >= 400 && error.statusCode < 500) {
throw error;
}
if (isLastAttempt) throw error;
// Exponential backoff: 1s, 2s, 4s
await delay(Math.pow(2, attempt) * 1000);
}
}
}
// Usage
const { question } = await fetchWithRetry(() => api.getNextQuestion(skillId, algorithmId, sessionId));
Auth Token Expiry
async function makeRequest<T>(requestFn: () => Promise<T>): Promise<T> {
try {
return await requestFn();
} catch (error) {
// Token expired - refresh and retry once
if (error.statusCode === 401) {
await refreshToken();
return await requestFn();
}
throw error;
}
}
// With Amplify
async function refreshToken() {
await fetchAuthSession({ forceRefresh: true });
}
Handling API Errors
// Error response formats
// Standard error:
// { statusCode: 400, message: "Bad Request", error: "sessionId is required" }
// Validation error:
// { statusCode: 400, message: "Validation failed", errors: [{ field: "timeTookToAnswerMs", message: "..." }] }
try {
const feedback = await api.submitAnswer(/* ... */);
} catch (error) {
if (error.statusCode === 400) {
// Bad request - likely our bug
console.error('Invalid request:', error.message);
showMessage('Something went wrong. Please restart.');
} else if (error.statusCode === 401) {
// Auth expired
showMessage('Session expired. Please sign in again.');
redirectToLogin();
} else if (error.statusCode >= 500) {
// Server error - retryable
showMessage('Server error. Retrying...');
await retry();
}
}
Handling skillAvailability
const { question, skillAvailability } = await api.getNextQuestion(/* ... */);
switch (skillAvailability) {
case 'Available':
displayQuestion(question);
break;
case 'OnCooldown':
// Spaced repetition enforcing wait
showMessage('All facts are resting. Come back later!');
endSession('cooldown');
break;
case 'Completed':
// 100% mastery
showMessage('Skill mastered! 🎉');
endSession('completed');
break;
case 'Unavailable':
showMessage('Skill not available.');
endSession('unavailable');
break;
}
Best Practices
Timing Accuracy
// ✅ Good: Track per-question timing
const start = Date.now();
const timeTookMs = Date.now() - start;
// ❌ Bad: Use fixed values
const timeTookMs = 3000; // API needs real timing
Active Session Time
// ✅ Good: Only count learning time
onQuestionDisplay(() => startTimer());
onPauseMenu(() => stopTimer());
// ❌ Bad: Count everything including menus
const activeTime = Date.now() - sessionStart; // Wrong!
Session ID Persistence
// ✅ Good: One sessionId per session
const sessionId = crypto.randomUUID();
// Use for all questions in this session
// ❌ Bad: New sessionId per question
await askQuestion(crypto.randomUUID()); // Wrong!
Error Recovery
// ✅ Good: Save state, offer recovery
try {
saveState(session);
await api.submitAnswer(/* ... */);
} catch (error) {
showDialog(['Retry', 'End Session', 'Menu']);
}
// ❌ Bad: Silent failure
catch (error) {
console.error(error); // User sees nothing
}
Related Docs
Getting Started
Authentication setup and basic learning loop. Start here if you're new to the API.
Interventions Guide
Detailed breakdown of each intervention type with UI implementation guidance and reference videos.
Timing Fields Guide
Critical timing implementation details for timeTookToAnswerMs and activeSessionDurationSec.
API Reference
Complete endpoint documentation. Generate a typed client from the OpenAPI spec to use in your code.