Skip to content
Learni
Voir tous les tutoriels
JavaScript

Comment implémenter un client xAPI en TypeScript 2026

Read in English

Introduction

xAPI, ou Experience API (aussi appelée Tin Can API), est le standard ouvert pour capturer et stocker des données d'apprentissage granulaires au-delà des simples quizzes SCORM. Contrairement à SCORM qui se limite aux LMS fermés, xAPI suit n'importe quelle expérience : apps mobiles, VR, jeux, micro-learning.

Pourquoi l'implémenter en 2026 ? Les LMS modernes (Moodle, Cornerstone) et LRS (Learning Record Stores) comme Learning Locker exigent xAPI pour l'interopérabilité. Ce tutoriel avancé vous guide pour créer un client TypeScript/Node.js complet : types stricts, envoi de statements, authentification Basic Auth, batches, validation JSON Schema et gestion d'erreurs.

À la fin, vous aurez un module réutilisable pour tracker des événements comme 'initialized', 'completed' ou 'answered'. Idéal pour intégrer dans une app React/Next.js ou un backend custom. Analogie : xAPI est comme un journal blockchain pour l'apprentissage – immuable, décentralisé et riche en métadonnées.

Prérequis

  • Node.js 20+ installé
  • Connaissances avancées en TypeScript (generics, interfaces)
  • Familiarité avec xAPI (statements, actor/verb/object)
  • Un compte LRS gratuit (ex: Learning Locker demo ou ADL LRS)
  • Outils : npm/yarn, VS Code avec ESLint/Prettier

Initialisation du projet

terminal
mkdir xapi-client-ts && cd xapi-client-ts
npm init -y
npm install typescript @types/node axios json-schema-validator
npm install -D ts-node nodemon @types/json-schema-validator
npx tsc --init

Ce script initialise un projet Node.js avec TypeScript, installe Axios pour les HTTP requests, un validateur JSON Schema pour xAPI, et ts-node pour exécuter directement le TS. Modifiez tsconfig.json pour inclure esModuleInterop: true et strict: true. Lancez avec npx ts-node index.ts.

Définition des types xAPI

types/xapi.ts
export interface Actor {
  mbox: string;
  name?: string;
  account?: { name: string; homePage: string };
}

export interface Verb {
  id: string;
  display: { [lang: string]: string };
}

export interface ActivityObject {
  id: string;
  definition: {
    name: { [lang: string]: string };
    type: string;
  };
}

export interface Statement {
  actor: Actor;
  verb: Verb;
  object: ActivityObject;
  timestamp?: string;
  id?: string;
  result?: { success: boolean; duration: string };
}

export interface XAPIError {
  error: string;
  message: string;
}

Ces interfaces TypeScript modélisent strictement un statement xAPI de base (niveau 1). Utilisez mbox pour l'actor (email IRI), des IRIs standard pour verbs (ex: 'http://adlnet.gov/expapi/verbs/initialized'). Ajoutez account pour les users anonymes. Cela prévient 80% des erreurs de formatage LRS.

Comprendre la structure d'un statement

Un statement xAPI suit le modèle Actor-Verb-Object : qui (actor), fait quoi (verb), sur quoi (object). Exemple concret : un learner ('user@example.com') initialise une activité ('http://example.com/quiz1').

  • Actor : Identifié par IRI (mbox, account, SHA1-hash pour anonymat).
  • Verb : IRI standard + traductions multilingues.
  • Object : Activité avec définition (nom, type).
Analogie : comme une phrase SQL INSERT, mais pour l'apprentissage. Les extensions (result, context) s'ajoutent pour la richesse.

Client xAPI avec authentification

client/xapi-client.ts
import axios, { AxiosError } from 'axios';
import type { Statement, XAPIError } from '../types/xapi';

const LRS_URL = 'https://lrs.adlnet.gov/xAPI/'; // Remplacez par votre LRS
const USERNAME = 'your-username';
const PASSWORD = 'your-password';

const auth = Buffer.from(`${USERNAME}:${PASSWORD}`).toString('base64');

export class XAPIClient {
  private baseURL: string;

  constructor(lrsURL: string) {
    this.baseURL = `${lrsURL}data/xAPI/statements`;
  }

  async sendStatement(statement: Statement): Promise<void> {
    try {
      const response = await axios.post(this.baseURL, statement, {
        headers: {
          'Content-Type': 'application/json',
          'Authorization': `Basic ${auth}`,
          'X-Experience-API-Version': '1.0.3'
        }
      });
      console.log('Statement ID:', response.data.id);
    } catch (error) {
      const err = error as AxiosError<XAPIError>;
      throw new Error(`xAPI Error: ${err.response?.data?.message || err.message}`);
    }
  }
}

Ce client encapsule l'envoi POST vers /data/xAPI/statements avec Basic Auth et header version xAPI 1.0.3. Utilisez Axios pour la gestion d'erreurs typée. Remplacez LRS_URL/credentials par les vôtres ; testez avec ADL LRS gratuit. Piège : oubliez le header version → rejet 400.

Exemple : Statement 'initialized'

examples/initialized.ts
import { XAPIClient } from '../client/xapi-client';
import type { Statement } from '../types/xapi';

const client = new XAPIClient('https://lrs.adlnet.gov/xAPI/');

const statement: Statement = {
  actor: {
    mbox: 'mailto:learner@example.com',
    name: 'Jean Dupont'
  },
  verb: {
    id: 'http://adlnet.gov/expapi/verbs/initialized',
    display: { 'fr': 'a initialisé', 'en-US': 'initialized' }
  },
  object: {
    id: 'http://example.com/activites/quiz-maths',
    definition: {
      name: { 'fr': 'Quiz de mathématiques', 'en-US': 'Math Quiz' },
      type: 'http://adlnet.gov/expapi/activities/cmi-interaction'
    }
  }
};

(async () => {
  await client.sendStatement(statement);
})();

Ce script envoie un statement 'initialized' complet et valide. Copiez-collez pour tester instantanément. Utilisez des IRIs officiels (adlnet.gov) pour interopérabilité maximale. Résultat : LRS retourne l'UUID du statement stocké.

Gestion des résultats et contextes

Result ajoute succès/durée : { success: true, duration: 'PT5M' } (ISO 8601). Context pour registration-ID ou instructor. Étendez l'interface Statement pour cela.

Pour 'completed' : verb http://adlnet.gov/expapi/verbs/completed, result avec score.

Statement 'completed' avec result

examples/completed.ts
import { XAPIClient } from '../client/xapi-client';
import type { Statement } from '../types/xapi';

const client = new XAPIClient('https://lrs.adlnet.gov/xAPI/');

const statement: Statement = {
  actor: {
    mbox: 'mailto:learner@example.com',
    name: 'Jean Dupont'
  },
  verb: {
    id: 'http://adlnet.gov/expapi/verbs/completed',
    display: { 'fr': 'a terminé', 'en-US': 'completed' }
  },
  object: {
    id: 'http://example.com/activites/quiz-maths',
    definition: {
      name: { 'fr': 'Quiz de mathématiques' },
      type: 'http://adlnet.gov/expapi/activities/cmi-interaction'
    }
  },
  result: {
    success: true,
    duration: 'PT10M30S',
    score: { scaled: 0.85, raw: 17, min: 0, max: 20 }
  }
};

(async () => {
  await client.sendStatement(statement);
})();

Ajoute un result complet avec score scaled (0-1). Durée en ISO 8601. Parfait pour clore une session. Piège : score sans min/max → validation LRS échoue.

Envoi en batch (voided)

examples/batch.ts
import { XAPIClient } from '../client/xapi-client';
import type { Statement } from '../types/xapi';

const client = new XAPIClient('https://lrs.adlnet.gov/xAPI/');

const statements: Statement[] = [
  // Statement initialized (du précédent exemple)
  {
    actor: { mbox: 'mailto:learner@example.com', name: 'Jean Dupont' },
    verb: { id: 'http://adlnet.gov/expapi/verbs/initialized', display: { 'fr': 'a initialisé' } },
    object: { id: 'http://example.com/activites/quiz-maths', definition: { name: { 'fr': 'Quiz maths' }, type: 'http://adlnet.gov/expapi/activities/cmi-interaction' } }
  },
  // Voided pour annuler
  {
    actor: { mbox: 'mailto:system@example.com' },
    verb: { id: 'http://adlnet.gov/expapi/verbs/voided', display: { 'fr': 'a annulé' } },
    object: { id: '00000000-0000-0000-0000-000000000000', objectType: 'StatementRef' } // Référence UUID du statement à voider
  }
];

(async () => {
  for (const stmt of statements) {
    await client.sendStatement(stmt);
  }
  console.log('Batch envoyé');
})();

Envoie un batch séquentiel : initialized puis voided (annulation via StatementRef). Pour vrai batch, POST [{...}] à /statements (étendez la méthode). Utilisez pour corrections en prod.

Validation JSON Schema

utils/validator.ts
import Ajv, { JSONSchemaType } from 'ajv';
import type { Statement } from '../types/xapi';

const ajv = new Ajv({ allErrors: true });

const schema: JSONSchemaType<Statement> = {
  type: 'object',
  properties: {
    actor: { type: 'object', properties: { mbox: { type: 'string', format: 'email' } } },
    verb: { type: 'object', properties: { id: { type: 'string', format: 'uri' } } },
    object: { type: 'object', properties: { id: { type: 'string', format: 'uri' } } }
  },
  required: ['actor', 'verb', 'object'],
  additionalProperties: false
};

const validate = ajv.compile(schema);

export function validateStatement(statement: unknown): statement is Statement {
  return validate(statement);
}

// Usage
if (!validateStatement(myStatement)) {
  console.error(validate.errors);
  throw new Error('Invalid statement');
}

Intégrez Ajv pour valider avant envoi contre un schema minimal. Formats 'email'/'uri' catch les erreurs courantes. Ajoutez à sendStatement : if (!validateStatement(statement)) throw .... Essentiel en prod pour éviter 400 LRS.

Bonnes pratiques

  • Toujours valider : Utilisez JSON Schema ou Zod avant envoi.
  • Anonymisez : Préférez account ou SHA1-mbox pour RGPD.
  • Batches intelligents : POST arrays jusqu'à 100 statements pour perf.
  • Retry avec backoff : Implémentez exponential retry sur 5xx.
  • Logs structurés : Stockez statement IDs pour audits.

Erreurs courantes à éviter

  • Mauvais IRI : Verbs non-standard → rejet LRS (utilisez adlnet.gov).
  • Pas d'auth : 401 Unauthorized ; encodez Basic Auth correctement.
  • Timestamp absent : LRS l'ajoute, mais forcez ISO pour cohérence.
  • Object sans definition : 400 Bad Request ; toujours nom/type.

Pour aller plus loin

Intégrez xAPI dans React avec @xapi/xapiwrapper. Étudiez cmi5 pour packaging. Implémentez un LRS custom avec Rust/ScyllaDB.

Découvrez nos formations Learni sur xAPI et eLearning pour certifications avancées.