IndexedDB Guide
Complete guide to using IndexedDB for powerful client-side storage
IndexedDB is your browser's built-in mini database — and it's awesome for storing large amounts of structured data right in the user's device. Whether you're building offline-first apps or need a smart way to cache data, IndexedDB's got your back.
Let's break it down into digestible, friendly bits
Why Use IndexedDB?
Here's what makes IndexedDB a standout tool:
1. Structured, Object-Based Storage
Think of it like a spreadsheet or a mini database:
- Data is stored as JavaScript objects (not just strings like in
localStorage) - You use keys to access or index that data
- Supports complex data structures and relationships
2. Asynchronous by Design
- All operations (like
put,get, ordelete) run asynchronously, meaning they don't block your UI - Perfect for maintaining a smooth, snappy user experience
3. Stores a LOT of Data
- Need more space than cookies or
localStorage? IndexedDB lets you store hundreds of MBs — or even more — depending on the browser
4. Cross-Tab Sharing (No Same-Origin Policy Limits)
- IndexedDB is origin-specific but can be accessed across tabs/windows of the same site. Great for syncing data!
5. Transactional Integrity
- Operations run inside transactions, which means if one step fails, the whole thing rolls back. This keeps your data consistent and safe
6. Powerful Indexing
- You can create custom indexes on your object properties to query large datasets quickly — like searching for a user by email
7. Works Offline Like a Champ
- Combine IndexedDB with service workers or PWA techniques and boom — your app can work without the internet
8. Private by Design
- Data is locked to your app's origin (protocol + domain + port), so other sites can't touch it
9. Fully Supported
- Works smoothly in all major browsers: Chrome, Firefox, Edge, Safari, and more
Getting Started with IndexedDB
Basic Setup
// Open or create a database
const request = indexedDB.open('MyAppDB', 1);
request.onerror = (event) => {
console.error('Database error:', event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
console.log('Database opened successfully');
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store (like a table)
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', { keyPath: 'id' });
// Create indexes for faster queries
userStore.createIndex('email', 'email', { unique: true });
userStore.createIndex('name', 'name', { unique: false });
}
};Wrapper Class for Easier Usage
class IndexedDBManager {
constructor(dbName, version) {
this.dbName = dbName;
this.version = version;
this.db = null;
}
async init() {
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) => {
const db = event.target.result;
// Create stores
if (!db.objectStoreNames.contains('users')) {
const userStore = db.createObjectStore('users', { keyPath: 'id' });
userStore.createIndex('email', 'email', { unique: true });
}
if (!db.objectStoreNames.contains('posts')) {
const postStore = db.createObjectStore('posts', { keyPath: 'id' });
postStore.createIndex('userId', 'userId', { unique: false });
postStore.createIndex('createdAt', 'createdAt', { unique: false });
}
};
});
}
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, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async getAll(storeName) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const request = store.getAll();
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async update(storeName, data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.put(data);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async delete(storeName, key) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readwrite');
const store = transaction.objectStore(storeName);
const request = store.delete(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async queryByIndex(storeName, indexName, value) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([storeName], 'readonly');
const store = transaction.objectStore(storeName);
const index = store.index(indexName);
const request = index.getAll(value);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
}Real-World Examples
User Management System
// Initialize the database
const dbManager = new IndexedDBManager('UserApp', 1);
// User operations
class UserService {
async addUser(user) {
const userData = {
id: Date.now().toString(),
...user,
createdAt: new Date().toISOString()
};
await dbManager.add('users', userData);
return userData;
}
async getUserById(id) {
return await dbManager.get('users', id);
}
async getUserByEmail(email) {
const users = await dbManager.queryByIndex('users', 'email', email);
return users[0] || null;
}
async updateUser(user) {
return await dbManager.update('users', user);
}
async deleteUser(id) {
return await dbManager.delete('users', id);
}
async getAllUsers() {
return await dbManager.getAll('users');
}
}
// Usage example
const userService = new UserService();
// Add a user
const newUser = await userService.addUser({
name: 'John Doe',
email: 'john@example.com',
age: 30
});
// Find user by email
const user = await userService.getUserByEmail('john@example.com');Blog Post System with Relationships
class PostService {
async addPost(post) {
const postData = {
id: Date.now().toString(),
...post,
createdAt: new Date().toISOString()
};
await dbManager.add('posts', postData);
return postData;
}
async getPostsByUser(userId) {
return await dbManager.queryByIndex('posts', 'userId', userId);
}
async getRecentPosts(limit = 10) {
const allPosts = await dbManager.getAll('posts');
return allPosts
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt))
.slice(0, limit);
}
}
// Usage
const postService = new PostService();
// Add a post
const post = await postService.addPost({
title: 'My First Post',
content: 'This is the content...',
userId: newUser.id,
tags: ['javascript', 'indexeddb']
});
// Get user's posts
const userPosts = await postService.getPostsByUser(newUser.id);Advanced Patterns
Caching API Responses
class APICache {
constructor() {
this.dbManager = new IndexedDBManager('APICache', 1);
}
async cacheResponse(url, data, ttl = 3600000) { // 1 hour default
const cacheEntry = {
url,
data,
timestamp: Date.now(),
ttl
};
await this.dbManager.update('apiCache', cacheEntry);
}
async getCachedResponse(url) {
const entry = await this.dbManager.get('apiCache', url);
if (!entry) return null;
// Check if cache is still valid
if (Date.now() - entry.timestamp > entry.ttl) {
await this.dbManager.delete('apiCache', url);
return null;
}
return entry.data;
}
async fetchWithCache(url, options = {}) {
// Try cache first
const cached = await this.getCachedResponse(url);
if (cached) {
return cached;
}
// Fetch from network
const response = await fetch(url, options);
const data = await response.json();
// Cache the response
await this.cacheResponse(url, data, options.ttl);
return data;
}
}Offline-First Data Sync
class OfflineSync {
constructor() {
this.dbManager = new IndexedDBManager('OfflineApp', 1);
this.syncQueue = [];
}
async addToSyncQueue(action) {
const syncItem = {
id: Date.now().toString(),
action,
timestamp: Date.now(),
status: 'pending'
};
await this.dbManager.add('syncQueue', syncItem);
}
async syncWhenOnline() {
if (!navigator.onLine) return;
const pendingItems = await this.dbManager.queryByIndex('syncQueue', 'status', 'pending');
for (const item of pendingItems) {
try {
// Attempt to sync with server
await this.performSync(item.action);
// Mark as completed
item.status = 'completed';
await this.dbManager.update('syncQueue', item);
} catch (error) {
console.error('Sync failed:', error);
item.status = 'failed';
await this.dbManager.update('syncQueue', item);
}
}
}
async performSync(action) {
// Implement your sync logic here
const response = await fetch('/api/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(action)
});
if (!response.ok) {
throw new Error('Sync failed');
}
return response.json();
}
}Common Use Cases
Here's where IndexedDB really shines:
- Caching API responses for offline or low-connectivity experiences
- Saving user-generated content like notes, drafts, or form data
- Storing large JSON datasets or file blobs
- Syncing local app state across tabs
- PWA offline functionality for mobile apps
- Game state persistence for browser games
- Analytics data buffering before sending to server
Important Considerations
Browser Support and Limitations
// Check for IndexedDB support
if (!window.indexedDB) {
console.warn('IndexedDB not supported, falling back to localStorage');
// Implement fallback
}
// Handle storage quota
async function checkStorageQuota() {
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
const usagePercent = (estimate.usage / estimate.quota) * 100;
if (usagePercent > 80) {
console.warn('Storage usage is high:', usagePercent.toFixed(1) + '%');
}
}
}Error Handling
class IndexedDBError extends Error {
constructor(message, originalError) {
super(message);
this.name = 'IndexedDBError';
this.originalError = originalError;
}
}
// Enhanced error handling
async function safeIndexedDBOperation(operation) {
try {
return await operation();
} catch (error) {
if (error.name === 'QuotaExceededError') {
throw new IndexedDBError('Storage quota exceeded', error);
} else if (error.name === 'VersionError') {
throw new IndexedDBError('Database version mismatch', error);
} else {
throw new IndexedDBError('IndexedDB operation failed', error);
}
}
}Performance Tips
- Use transactions efficiently - group related operations
- Create indexes wisely - only for frequently queried fields
- Clean up old data - implement TTL (Time To Live) for cached data
- Monitor storage usage - prevent quota exceeded errors
- Use cursors for large datasets - avoid loading everything into memory
Final Thoughts
If your app needs robust local storage and you're ready to go beyond key-value strings, IndexedDB is a fantastic choice. It might feel complex at first, but once you get the hang of it, it unlocks a whole new level of browser capabilities
Want to try it out? Tools like idb (a small wrapper library) can make working with IndexedDB even easier.
Happy caching!
Next: Broadcast Channel API - Learn how to communicate between browser tabs and windows.