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
}

Authentication setup and basic learning loop. Start here if you're new to the API.

Detailed breakdown of each intervention type with UI implementation guidance and reference videos.

Critical timing implementation details for timeTookToAnswerMs and activeSessionDurationSec.

Complete endpoint documentation. Generate a typed client from the OpenAPI spec to use in your code.