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
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
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).
Client xAPI avec authentification
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'
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
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)
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
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
accountou 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.