Browser APIs
IndexedDB
Client-side storage for structured data
Intermediate
browser storage offline performance
Definition
IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files and blobs. It uses object stores with indexes to enable high-performance searches, and works well with Web Workers and WebAssembly.
Basic Concepts
Database Structure
IndexedDB Database
├── Object Store: "users"
│ ├── Index: "email" (unique)
│ ├── Index: "age"
│ └── Records: { id, name, email, age }
├── Object Store: "products"
│ ├── Index: "category"
│ └── Records: { id, name, price, category }
└── Object Store: "orders"
├── Index: "userId"
├── Index: "date"
└── Records: { id, userId, items, date, total }
Opening a Database
Database Initialization
const DB_NAME = 'MyAppDB';
const DB_VERSION = 1;
const openRequest = indexedDB.open(DB_NAME, DB_VERSION);
openRequest.onerror = (event) => {
console.error('Failed to open database:', event.target.error);
};
openRequest.onsuccess = (event) => {
const db = event.target.result;
console.log('Database opened successfully');
// Store reference for later use
window.db = db;
};
// This fires when creating or upgrading database
openRequest.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object stores
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', {
keyPath: 'id',
autoIncrement: true
});
// Create indexes
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('age', 'age', { unique: false });
}
if (!db.objectStoreNames.contains('products')) {
const productStore = db.createObjectStore('products', {
keyPath: 'id'
});
productStore.createIndex('category', 'category');
}
};
CRUD Operations
Adding Data
function addUser(user) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.add(user);
request.onsuccess = () => {
console.log('User added with ID:', request.result);
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Usage
addUser({ name: 'John', email: 'john@example.com', age: 30 })
.then(id => console.log('Added user ID:', id))
.catch(err => console.error('Failed to add user:', err));
Reading Data
function getUser(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.get(id);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// Get by index
function getUserByEmail(email) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('email');
const request = index.get(email);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
Updating Data
function updateUser(user) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.put(user); // Updates if exists, adds if not
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
Deleting Data
function deleteUser(id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
const request = store.delete(id);
request.onsuccess = () => {
resolve();
};
request.onerror = () => {
reject(request.error);
};
});
}
Queries and Cursors
Getting All Records
function getAllUsers() {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
// With query
function getUsersByAgeRange(minAge, maxAge) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const index = store.index('age');
const range = IDBKeyRange.bound(minAge, maxAge);
const request = index.getAll(range);
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
Using Cursors for Large Datasets
async function* userIterator() {
const transaction = db.transaction(['users'], 'readonly');
const store = transaction.objectStore('users');
const request = store.openCursor();
while (true) {
const result = await new Promise((resolve, reject) => {
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!result) break;
yield result.value;
result.continue();
}
}
// Usage
for await (const user of userIterator()) {
console.log(user);
// Process without loading all into memory
}
Promisified API
Helper Library Pattern
class IndexedDBWrapper {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async open(upgradeCallback) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.version);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
if (upgradeCallback) {
upgradeCallback(event.target.result, event.oldVersion, event.newVersion);
}
};
});
}
async add(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.add(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async get(storeName, id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
// ... other methods
}
// Usage
const db = new IndexedDBWrapper('MyApp', 1);
await db.open((db, oldVersion, newVersion) => {
if (!db.objectStoreNames.contains('items')) {
db.createObjectStore('items', { keyPath: 'id' });
}
});
await db.add('items', { id: 1, name: 'Item 1' });
Best Practices
Error Handling
function handleDBError(error) {
switch (error.name) {
case 'QuotaExceededError':
console.error('Storage quota exceeded');
// Clear old data or notify user
break;
case 'VersionError':
console.error('Database version mismatch');
break;
case 'AbortError':
console.error('Transaction was aborted');
break;
default:
console.error('Database error:', error);
}
}
Transaction Management
// Keep transactions short
function updateMultipleUsers(users) {
const transaction = db.transaction(['users'], 'readwrite');
const store = transaction.objectStore('users');
// Queue all operations
users.forEach(user => {
store.put(user);
});
// Wait for completion
return new Promise((resolve, reject) => {
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
Storage Limits
// Check available storage
async function checkStorage() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const { usage, quota } = await navigator.storage.estimate();
const percentUsed = (usage / quota) * 100;
console.log(`Using ${percentUsed.toFixed(2)}% of quota`);
if (percentUsed > 90) {
// Clean up old data
await cleanupOldData();
}
}
}
Browser Support
// Feature detection
if ('indexedDB' in window) {
// Use IndexedDB
} else {
// Fallback to localStorage or cookies
console.warn('IndexedDB not supported');
}
// For older browsers, use a polyfill
// or wrap with localStorage fallback
Key Takeaway
IndexedDB is the solution for storing significant amounts of structured data client-side. It supports indexes, transactions, and works in Web Workers. Use wrapper libraries or create promisified wrappers for cleaner code. Always handle quota exceeded errors and keep transactions short for optimal performance.