IndexedDB: A Comprehensive Guide and Wrapper Implementation

What is IndexedDB?

IndexedDB is a native browser API designed for storing large amounts of structured data on the client side. Unlike traditional storage solutions such as localStorage or sessionStorage, IndexedDB functions as a full-featured in-browser database capable of handling substantial volumes of data with complex querying capabilities. This makes it particularly valuable for applications requiring offline functionality, client-side caching, or persistent storage of user-generated content.

The architecture of IndexedDB revolves around object stores and indexes. Object stores serve as the primary containers for data (similar to tables in relational databases), while indexes provide efficiant lookup mechanisms for searching and filtering stored records.

Core Features

  • Massive Storage Capacity: Supports storing significantly larger datasets compared to localStorage.
  • Non-Blocking Operations: Uses an asynchronous API design that prevents UI freezes during database operations.
  • Transaction Support: Ensures data integrity through atomic transactions with rollback capabilities.
  • Key-Value Architecture: Enables rapid data retrieval using unique keys assigned to each stored object.

Getting Started with IndexedDB

The IndexedDB API provides methods like open(), add(), get(), and remove() for performing CRUD operations. Here's a foundational example demonstrating database initialization:

// Initialize or open a database
const database = indexedDB.open('appData', 1);

// Handle database schema creation or upgrades
database.onupgradeneeded = function(event) {
    database = event.target.result;
    // Establish object store for storing records
    const store = database.createObjectStore('products', { keyPath: 'productId' });
    store.createIndex('category', 'category', { unique: false });
};

// Confirm successful connection
database.onsuccess = function(event) {
    database = event.target.result;
    console.log('Database connection established');
};

// Handle connection failures
database.onerror = function(event) {
    console.error('Failed to open database', event);
};

Building an IndexedDB Wrapper

Creating a wrapper class abstracts away the verbose native API, providing clean methods for common operations. Below is a practical implementation:

class DBManager {
    constructor(databaseName, versionNumber) {
        this.databaseName = databaseName;
        this.versionNumber = versionNumber;
        this.connection = null;
        this._initializeConnection();
    }

    _initializeConnection() {
        const openRequest = indexedDB.open(this.databaseName, this.versionNumber);

        openRequest.onupgradeneeded = (event) => {
            const db = event.target.result;
            // Set up object stores during version migration
            if (!db.objectStoreNames.contains('products')) {
                const store = db.createObjectStore('products', { keyPath: 'productId' });
                store.createIndex('category', 'category', { unique: false });
            }
        };

        openRequest.onsuccess = (event) => {
            this.connection = event.target.result;
            console.log('Connection ready');
        };

        openRequest.onerror = (event) => {
            console.error('Connection failed', event);
        };
    }

    // Insert or modify records
    saveRecord(storeName, record) {
        const transaction = this.connection.transaction([storeName], 'readwrite');
        const store = transaction.objectStore(storeName);
        const request = store.put(record);

        request.onsuccess = () => {
            console.log('Record saved successfully');
        };

        request.onerror = (event) => {
            console.error('Save operation failed', event);
        };
    }

    // Retrieve specific record
    fetchRecord(storeName, primaryKey) {
        return new Promise((resolve, reject) => {
            const transaction = this.connection.transaction([storeName], 'readonly');
            const store = transaction.objectStore(storeName);
            const request = store.get(primaryKey);

            request.onsuccess = (event) => {
                resolve(event.target.result);
            };

            request.onerror = () => {
                reject('Failed to fetch record');
            };
        });
    }

    // Remove specific record
    removeRecord(storeName, primaryKey) {
        const transaction = this.connection.transaction([storeName], 'readwrite');
        const store = transaction.objectStore(storeName);
        const request = store.delete(primaryKey);

        request.onsuccess = () => {
            console.log('Record removed successfully');
        };

        request.onerror = (event) => {
            console.error('Remove operation failed', event);
        };
    }

    // Retrieve all records from store
    fetchAllRecords(storeName) {
        return new Promise((resolve, reject) => {
            const transaction = this.connection.transaction([storeName], 'readonly');
            const store = transaction.objectStore(storeName);
            const request = store.getAll();

            request.onsuccess = (event) => {
                resolve(event.target.result);
            };

            request.onerror = () => {
                reject('Failed to retrieve records');
            };
        });
    }
}

// Usage example
const manager = new DBManager('appData', 1);

manager.saveRecord('products', { productId: 'P001', name: 'Widget', category: 'electronics' });

manager.fetchRecord('products', 'P001').then((result) => {
    console.log('Retrieved item:', result);
}).catch((err) => {
    console.error(err);
});

manager.fetchAllRecords('products').then((items) => {
    console.log('All products:', items);
});

Implementation Breakdown

  1. _initializeConnection(): Establishes the database connection and handles version mgirations through the onupgradeneeded callback, which is the appropriate place for schema modifications.
  2. saveRecord(): Handles both insertion and updates—using put() automatically replcaes existing records with matching keys.
  3. fetchRecord(): Retrieves individual records by their primary key, returning a Promise for clean async/await compatibility.
  4. removeRecord(): Permanently deletes records matching the specified key.
  5. fetchAllRecords(): Retrieves the complete dataset from a given object store, useful for bulk operations and initial data loading.

Tags: indexeddb javascript client-side-storage web-apis database

Posted on Sat, 16 May 2026 01:36:33 +0000 by tuf-web.co.uk