// apis
Tutoriel IndexedDB : construire une application de notes qui fonctionne hors ligne
Un tutoriel IndexedDB pas a pas. Ouvrir une base de donnees, creer un object store, ajouter et lire des enregistrements, interroger par index, supprimer des entrees et gerer les montees de version, le tout dans une petite application de notes executable dans le navigateur.
Le guide conceptuel explique ce qu'est IndexedDB et quand l'utiliser. Ce tutoriel construit une petite application de notes en partant de zero pour que vous voyiez chaque etape : ouvrir une base de donnees, creer un store, ecrire des enregistrements, les relire, interroger par index et supprimer des entrees.
Tout s'execute dans le navigateur. Pas d'outils de build, pas de serveur, pas de dependances. Copiez chaque bloc dans un seul fichier HTML et ouvrez-le.
Etape 1 : ouvrir la base de donnees
Toute interaction avec IndexedDB commence par indexedDB.open(). Vous passez un nom et un numero de version. Si la base n'existe pas encore, ou si votre version est superieure a celle sur le disque, le navigateur declenche upgradeneeded.
function openDB() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('notes-app', 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains('notes')) {
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('by_tag', 'tag', { unique: false });
store.createIndex('by_date', 'createdAt', { unique: false });
}
};
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Quelques points a remarquer. keyPath: 'id' indique a IndexedDB quelle propriete de chaque objet est la cle primaire. autoIncrement: true fait generer cette cle par la base de donnees. Les deux appels createIndex vous permettront d'interroger les notes par tag ou par date sans parcourir tous les enregistrements.
Etape 2 : ajouter un enregistrement
Ecrire dans IndexedDB se fait toujours dans une transaction. Vous en ouvrez une sur l'object store dont vous avez besoin, obtenez une reference a ce store, et appelez add() ou put().
async function addNote(db, text, tag) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const record = {
text,
tag: tag || 'general',
createdAt: new Date().toISOString(),
};
const request = store.add(record);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} add() leve une erreur si un enregistrement avec la meme cle existe deja. put() ecrase a la place. Pour les nouveaux enregistrements, add() est plus sur car il attrape les doublons accidentels.
Etape 3 : lire tous les enregistrements
Pour tout lire dans un store, ouvrez une transaction readonly et appelez getAll() :
async function getAllNotes(db) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly');
const store = tx.objectStore('notes');
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Si vous n'avez besoin que d'un enregistrement et que vous connaissez sa cle, utilisez store.get(key) a la place. Il renvoie un seul objet plutot qu'un tableau.
Etape 4 : interroger par index
Les index permettent de trouver des enregistrements sans parcourir tout le store. Pour obtenir toutes les notes taguees "work" :
async function getNotesByTag(db, tag) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readonly');
const store = tx.objectStore('notes');
const index = store.index('by_tag');
const request = index.getAll(tag);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
} Vous pouvez aussi utiliser un key range pour interroger une plage de valeurs. Par exemple, pour obtenir les notes creees apres une certaine date :
const range = IDBKeyRange.lowerBound('2026-01-01T00:00:00.000Z');
const request = store.index('by_date').getAll(range); Les quatre constructeurs de range sont lowerBound, upperBound, bound (les deux extremites) et only (correspondance exacte). Chacun accepte un booleen optionnel pour exclure la valeur limite.
Etape 5 : mettre a jour un enregistrement
La mise a jour utilise put(). Vous devez inclure la propriete cle (id dans notre schema) pour qu'IndexedDB sache quel enregistrement ecraser :
async function updateNote(db, id, newText) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const getReq = store.get(id);
getReq.onsuccess = () => {
const note = getReq.result;
if (!note) { reject(new Error('Not found')); return; }
note.text = newText;
const putReq = store.put(note);
putReq.onsuccess = () => resolve();
putReq.onerror = () => reject(putReq.error);
};
getReq.onerror = () => reject(getReq.error);
});
} Etape 6 : supprimer un enregistrement
Appelez store.delete(key) dans une transaction readwrite :
async function deleteNote(db, id) {
return new Promise((resolve, reject) => {
const tx = db.transaction('notes', 'readwrite');
const store = tx.objectStore('notes');
const request = store.delete(id);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
} Pour vider tous les enregistrements d'un store, utilisez store.clear() a la place.
Etape 7 : gerer les montees de version
Quand vous devez ajouter un nouvel index ou un nouveau store apres que les utilisateurs ont deja la version 1, incrementez le numero de version et gerez la migration dans onupgradeneeded :
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
const store = db.createObjectStore('notes', {
keyPath: 'id',
autoIncrement: true,
});
store.createIndex('by_tag', 'tag', { unique: false });
store.createIndex('by_date', 'createdAt', { unique: false });
}
if (oldVersion < 2) {
const tx = event.target.transaction;
const store = tx.objectStore('notes');
store.createIndex('by_priority', 'priority', { unique: false });
}
}; Verifiez event.oldVersion pour n'executer que les migrations que l'utilisateur n'a pas encore vues. Un utilisateur qui passe de 0 (pas de base) execute les deux blocs. Un utilisateur qui passe de 1 n'execute que le second.
Assembler le tout
Voici un script minimal qui exerce toutes les fonctions ci-dessus :
(async () => {
const db = await openDB();
const id = await addNote(db, 'Buy groceries', 'personal');
console.log('Added note with id:', id);
const all = await getAllNotes(db);
console.log('All notes:', all);
const tagged = await getNotesByTag(db, 'personal');
console.log('Personal notes:', tagged);
await updateNote(db, id, 'Buy groceries and cook dinner');
console.log('Updated note', id);
await deleteNote(db, id);
console.log('Deleted note', id);
})(); Ouvrez la console du navigateur pour voir la sortie. Chaque operation est asynchrone, chaque ecriture passe par une transaction, et les donnees survivent aux rechargements de page sans aucun serveur.
Erreurs courantes
| Erreur | Ce qui se passe | Correction |
|---|---|---|
Creer un store en dehors de onupgradeneeded | InvalidStateError | Tous les changements de schema vont dans onupgradeneeded |
Utiliser await dans un callback de transaction | La transaction se ferme automatiquement avant que l'await se resolve | Gardez toutes les operations sur le store synchrones dans un meme tick de transaction, ou ouvrez une nouvelle transaction apres l'await |
| Oublier d'incrementer le numero de version | onupgradeneeded ne se declenche jamais | Incrementez l'entier de version chaque fois que vous modifiez le schema |
| Passer une chaine la ou une cle attend un nombre | Enregistrement non trouve | Respectez le type : si autoIncrement genere des nombres, interrogez avec un nombre |
IndexedDB est verbeux, mais chaque element a une raison d'etre : la version controle votre schema, les transactions protegent vos donnees, et les index gardent les lectures rapides. Une fois ces sept etapes devenues routine, vous avez tout ce qu'il faut pour stocker des donnees structurees cote client, que ce soit pour le hors ligne, le cache ou un etat qui survit a la session.