Building applications that work seamlessly offline has become a key requirement in modern web development. IndexedDB offers a powerful solution for developers looking to store and manage large amounts of data directly in the browser. This client-side NoSQL database allows for efficient data handling, making it ideal for web applications that need offline functionality. In this article, we’ll explore the basics of IndexedDB, its key features, and walk you through the steps to set it up and use it in your web projects.
What is IndexedDB?
IndexedDB is a powerful client-side NoSQL database that enables web applications to store and manage structured data directly in the browser. It provides an efficient solution for handling large datasets locally, making it ideal for offline functionality and data-heavy applications.

One of the main advantages of IndexedDB is its ability to support offline web applications. It allows developers to store substantial amounts of data, use indexing for fast retrieval, manage transactions, and navigate data using cursors. These features make it an excellent choice for handling complex data in web apps.
Most modern web browsers, including Chrome, Firefox, Safari, and Edge, support IndexedDB. However, it’s important to ensure that the version of IndexedDB is compatible with the browser you’re targeting for your web application.
To begin using IndexedDB, the first step is to open a connection to the database, which is necessary to create and interact with it.

Getting Started with IndexedDB: A Comprehensive Guide
IndexedDB is a powerful client-side storage solution for web applications. This guide will walk you through everything from basic setup to advanced techniques and real-world implementation.
Setting Up IndexedDB
Creating and Opening a Database
To create a new database in IndexedDB, you'll use the indexedDB.open()
method:
const request = indexedDB.open("MyDatabase", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object stores and indexes here
};
The method takes two parameters: the database name and version number. To open an existing database, you can simply call the method without specifying a version number.
Database Versioning
IndexedDB supports versioning to handle schema changes. When you open a database with a higher version number than the existing one, the onupgradeneeded
event is triggered, allowing you to update your database schema.
Creating Object Stores and Indexes
Object stores are containers for your data in IndexedDB:
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create an object store with a key path
const objectStore = db.createObjectStore("customers", { keyPath: "id" });
// Create an index for efficient querying
objectStore.createIndex("name", "name", { unique: false });
};
Working with Transactions
Understanding Transaction Basics
Transactions in IndexedDB maintain data integrity by grouping multiple operations into a single, atomic unit. They ensure that either all changes are applied or none are.
Creating and Managing Transactions
const transaction = db.transaction(["customers"], "readwrite");
const objectStore = transaction.objectStore("customers");
transaction.oncomplete = (event) => {
console.log("Transaction completed successfully");
};
transaction.onerror = (event) => {
console.error("Transaction failed");
};
Error Handling and Rolling Back
If an operation fails, you can use the abort()
method to roll back the entire transaction:
try {
// Perform operations
} catch (error) {
transaction.abort();
console.error("Transaction aborted:", error);
}
Data Operations in IndexedDB
Adding Data
const customerData = { id: "00001", name: "John Doe", email: "john@example.com" };
const request = objectStore.add(customerData);
request.onsuccess = (event) => {
console.log("Data added successfully");
};
Retrieving Data
Basic data retrieval by key:
const request = objectStore.get("00001");
request.onsuccess = (event) => {
console.log("Customer data:", request.result);
};
Filtering with Indexes
const index = objectStore.index("name");
const request = index.get("John Doe");
request.onsuccess = (event) => {
console.log("Found by name:", request.result);
};
Advanced Querying Methods
For complex queries, IndexedDB offers range queries and compound queries using methods like openCursor()
and IDBKeyRange
:
const range = IDBKeyRange.bound("A", "F"); // Names starting with A through F
const request = index.openCursor(range);
Updating Records
const updateData = { id: "00001", name: "John Smith", email: "john@example.com" };
const request = objectStore.put(updateData);
Deleting Records
const request = objectStore.delete("00001");
request.onsuccess = (event) => {
console.log("Record deleted");
};
Working with Cursors
Understanding Cursor Functionality
Cursors allow you to efficiently iterate over records in an object store or index, providing a way to traverse and manipulate data.
Navigating Through Records
const request = objectStore.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
console.log("Key:", cursor.key, "Value:", cursor.value);
cursor.continue(); // Move to the next record
} else {
console.log("No more records");
}
};
Modifying Data Using Cursors
const transaction = db.transaction(["customers"], "readwrite");
const objectStore = transaction.objectStore("customers");
const request = objectStore.openCursor();
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
if (cursor.value.status === "inactive") {
const updateData = cursor.value;
updateData.status = "active";
cursor.update(updateData);
}
cursor.continue();
}
};
Schema Management and Upgrades
Upgrading Database Schema
When your application evolves, you may need to modify your database schema:
const request = indexedDB.open("MyDatabase", 2); // Increase version number
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Check if the object store exists
if (!db.objectStoreNames.contains("newStore")) {
db.createObjectStore("newStore", { keyPath: "id" });
}
};
Migrating Data During Upgrades
request.onupgradeneeded = (event) => {
const db = event.target.result;
const oldVersion = event.oldVersion;
if (oldVersion < 1) {
// First version setup
}
if (oldVersion < 2) {
// Migrate data to new schema
const transaction = event.target.transaction;
const oldStore = transaction.objectStore("oldStore");
const newStore = db.createObjectStore("newStore", { keyPath: "id" });
oldStore.openCursor().onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
newStore.add(cursor.value);
cursor.continue();
}
};
}
};
Performance Optimization
Efficient Bulk Operations
For better performance, use bulk operations when dealing with multiple records:
const transaction = db.transaction(["customers"], "readwrite");
const store = transaction.objectStore("customers");
// Add multiple records in a single transaction
customerList.forEach(customer => {
store.add(customer);
});
Leveraging Indexes for Faster Queries
Create indexes on frequently queried properties to ensure faster data retrieval:
objectStore.createIndex("email", "email", { unique: true });
objectStore.createIndex("lastLogin", "lastLogin", { unique: false });
Connection Management Best Practices
Open connections only when necessary and close them when done:
let db;
function openDB() {
const request = indexedDB.open("MyDatabase", 1);
request.onsuccess = (event) => {
db = event.target.result;
};
return request;
}
// When you're done with the database
function closeDB() {
if (db) {
db.close();
db = null;
}
}
Real-World Example: Task Manager with Offline Support
Database Setup
const request = indexedDB.open("TaskManagerDB", 1);
request.onupgradeneeded = (event) => {
const db = event.target.result;
const taskStore = db.createObjectStore("tasks", { keyPath: "id", autoIncrement: true });
// Create indexes for querying
taskStore.createIndex("status", "status", { unique: false });
taskStore.createIndex("dueDate", "dueDate", { unique: false });
};
Adding Tasks
function addTask(taskData) {
const transaction = db.transaction(["tasks"], "readwrite");
const taskStore = transaction.objectStore("tasks");
return new Promise((resolve, reject) => {
const request = taskStore.add({
title: taskData.title,
description: taskData.description,
status: "pending",
dueDate: taskData.dueDate,
createdAt: new Date()
});
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
Retrieving and Displaying Tasks
function getAllTasks() {
const transaction = db.transaction(["tasks"], "readonly");
const taskStore = transaction.objectStore("tasks");
return new Promise((resolve, reject) => {
const request = taskStore.openCursor();
const tasks = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
tasks.push(cursor.value);
cursor.continue();
} else {
resolve(tasks);
}
};
request.onerror = () => reject(request.error);
});
}
Updating and Deleting Tasks
function updateTaskStatus(id, newStatus) {
const transaction = db.transaction(["tasks"], "readwrite");
const taskStore = transaction.objectStore("tasks");
return new Promise((resolve, reject) => {
const getRequest = taskStore.get(id);
getRequest.onsuccess = () => {
const task = getRequest.result;
task.status = newStatus;
task.updatedAt = new Date();
const updateRequest = taskStore.put(task);
updateRequest.onsuccess = () => resolve(true);
updateRequest.onerror = () => reject(updateRequest.error);
};
getRequest.onerror = () => reject(getRequest.error);
});
}
Syncing with the Server
function syncWithServer() {
if (!navigator.onLine) {
return Promise.reject(new Error("No internet connection"));
}
return getAllTasks()
.then(tasks => {
// Filter tasks that need syncing
const unsynced = tasks.filter(task => !task.synced);
// Send to server using fetch API
return fetch('https://api.example.com/tasks/sync', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(unsynced)
});
})
.then(response => response.json())
.then(result => {
// Mark tasks as synced
const transaction = db.transaction(["tasks"], "readwrite");
const taskStore = transaction.objectStore("tasks");
result.syncedIds.forEach(id => {
const request = taskStore.get(id);
request.onsuccess = () => {
const task = request.result;
task.synced = true;
taskStore.put(task);
};
});
return result;
});
}
// Listen for online events to sync automatically
window.addEventListener('online', syncWithServer);
Integrating Apidog for API Management with IndexedDB
When building applications with IndexedDB for client-side storage, you'll often need to interact with back-end APIs for data synchronization. Apidog provides a seamless solution for managing these API interactions.

Why Apidog Enhances IndexedDB Development
As you develop offline-capable applications with IndexedDB, Apidog offers several benefits:
Real-Time Synchronization: Apidog ensures your API endpoints used for data syncing are always properly configured and tested, eliminating integration issues when your app goes online.
Mock API Responses: When developing offline functionality, Apidog's smart mock engines allow you to simulate API responses for testing your IndexedDB sync logic without a live server.

Collaborative API Design: When your team is simultaneously working on front-end storage and back-end APIs, Apidog facilitates real-time collaboration on API specifications.

By integrating Apidog into your development workflow, you create a seamless bridge between client-side storage and server-side processing, making your application more robust and easier to maintain.