IndexedDB Tutorial for Beginners: A Comprehensive Guide with Coding Examples

Amresh Kumar
11 min readJun 17, 2023

--

IndexedDB is a powerful client-side database technology that allows web applications to store and retrieve large amounts of structured data. In this tutorial, we will explore the fundamental concepts of IndexedDB and provide coding examples for each feature. By the end, you will have a solid understanding of how to use IndexedDB to build robust web applications that can work offline and efficiently manage data.

This three-part series will guide you through:

  1. The basics of IndexedDB
  2. In-depth exploration of cursors
  3. Comprehensive understanding of indexes in IndexedDB

Table of Contents:

  • Introduction to IndexedDB
    – What is IndexedDB?
    – Why use IndexedDB?
    – Browser Support
  • Setting Up IndexedDB
    – Creating a Database
    – Opening a Database
    – Database Versioning
  • Object Stores and Indexes
    – Creating Object Stores
    – Defining Indexes
    – Adding and Retrieving Data
  • Transactions
    – Introduction to Transactions
    – Creating and Committing Transactions
    – Handling Errors and Rollbacks
  • Querying and Filtering Data
    – Basic Querying
    – Filtering Data with Indexes
    – Advanced Querying Techniques
  • Deleting and Updating Data
    – Removing Records
    – Updating Records
  • Working with Cursors
    – Introduction to Cursors
    – Navigating Cursors
    – Modifying Data with Cursors
  • Handling Upgrades and Schema Changes
    – Upgrading Database Schema
    – Migrating Data
  • Best Practices and Performance Optimization
    – Using Bulk Operations
    – Index Optimization
    – Managing Connection Lifecycles
  • Real-World Examples
    – Building a Task Manager
    – Implementing Offline Support

IndexedDB is a versatile technology that empowers web developers to create offline-capable applications with robust data management capabilities. In this tutorial, we covered the foundational concepts of IndexedDB, including creating and upgrading databases, working with object stores and indexes, querying and filtering data, handling transactions, and optimizing performance. Armed with this knowledge, you can now leverage IndexedDB to build powerful web applications that provide a seamless user experience even in offline scenarios.

Remember to experiment and explore further to unlock the full potential of IndexedDB in your web development projects. Happy coding!

Introduction to IndexedDB:

What is IndexedDB?
IndexedDB is a client-side NoSQL database that allows web applications to store and retrieve structured data. It provides a robust and efficient way to manage large amounts of data locally within the browser.

Why use IndexedDB?
IndexedDB is particularly useful for web applications that need to work offline or have significant data storage requirements. It offers features such as data indexing, transactions, and cursor-based navigation, making it a powerful choice for managing data in web applications.

Browser Support:
IndexedDB is supported by modern web browsers, including Chrome, Firefox, Safari, and Edge. However, it is important to check the specific version of IndexedDB supported by each browser to ensure compatibility.

Setting Up IndexedDB:

Creating a Database:
To create a database in IndexedDB, you need to open a connection to it.

let request = indexedDB.open("myDatabase", 1);

request.onupgradeneeded = function(event) {
let db = event.target.result;
let objectStore = db.createObjectStore("myObjectStore", { keyPath: "id" });
objectStore.createIndex("nameIndex", "name", { unique: false });
};

request.onsuccess = function(event) {
let db = event.target.result;
// Database opened successfully
};

request.onerror = function(event) {
// Error occurred while opening the database
};

Opening a Database:
To open an existing database in IndexedDB, you can use the indexedDB.open() method.

let request = indexedDB.open("myDatabase");

request.onsuccess = function(event) {
let db = event.target.result;
// Database opened successfully
};

request.onerror = function(event) {
// Error occurred while opening the database
};

Database Versioning:
IndexedDB supports versioning to manage schema changes and upgrades. When opening a database, you can specify the version number. If the version number is higher than the existing database, the onupgradeneeded event is triggered.

let request = indexedDB.open("myDatabase", 2);

request.onupgradeneeded = function(event) {
let db = event.target.result;
// Perform schema changes or upgrades here
};

request.onsuccess = function(event) {
let db = event.target.result;
// Database opened successfully
};

request.onerror = function(event) {
// Error occurred while opening the database
};

Object Stores and Indexes:

Creating Object Stores:
Object stores in IndexedDB are containers that hold data. You can create an object store using the createObjectStore() method.

let objectStore = db.createObjectStore("myObjectStore", { keyPath: "id" });

Defining Indexes:
Indexes in IndexedDB allow efficient querying of data based on specific properties. You can create an index using the createIndex() method.

let objectStore = db.createObjectStore("myObjectStore");
objectStore.createIndex("nameIndex", "name", { unique: false });

Adding and Retrieving Data:
You can add data to an object store using the add() method and retrieve data using the get() method.

let transaction = db.transaction("myObjectStore", "readwrite");
let objectStore = transaction.objectStore("myObjectStore");

let data = { id: 1, name: "John Doe", age: 25 };
let addRequest = objectStore.add(data);

addRequest.onsuccess = function(event) {
// Data added successfully
};

let getRequest = objectStore.get(1);

getRequest.onsuccess = function(event) {
let result = event.target.result;
// Access the retrieved data
};

This covers the first part of the tutorial. The subsequent parts will delve deeper into transactions, querying and filtering data, deleting and updating data, working with cursors, handling upgrades and schema changes, and best practices for performance optimization.

Transactions:

Introduction to Transactions:
Transactions in IndexedDB ensure data consistency and integrity by grouping a series of database operations into a single atomic unit. They provide a way to perform multiple database operations as a single logical unit of work.

Creating and Committing Transactions:
To create a transaction, you need to specify the object store(s) you want to operate on and the access mode (readOnly or readWrite).

let transaction = db.transaction(["myObjectStore"], "readwrite");

To commit a transaction, you can use the complete event handler. If the transaction encounters an error, the error event handler is triggered, allowing you to handle the error gracefully.

transaction.oncomplete = function(event) {
// Transaction committed successfully
};

transaction.onerror = function(event) {
// Error occurred while committing the transaction
};

Handling Errors and Rollbacks:
If any operation within a transaction fails, the entire transaction can be rolled back to maintain data consistency. You can use the abort() method to explicitly roll back a transaction.

transaction.onerror = function(event) {
// Error occurred while performing a transaction operation
transaction.abort();
};

Querying and Filtering Data:

Basic Querying:
IndexedDB provides methods to retrieve data based on keys, indexes, or object store cursors. Here’s an example of retrieving data by key.

let transaction = db.transaction("myObjectStore", "readonly");
let objectStore = transaction.objectStore("myObjectStore");

let getRequest = objectStore.get(1);

getRequest.onsuccess = function(event) {
let result = event.target.result;
// Access the retrieved data
};

Filtering Data with Indexes:
Indexes in IndexedDB allow efficient filtering and querying of data based on specific properties. Here’s an example of querying data using an index.

let transaction = db.transaction("myObjectStore", "readonly");
let objectStore = transaction.objectStore("myObjectStore");
let index = objectStore.index("nameIndex");

let getRequest = index.get("John Doe");

getRequest.onsuccess = function(event) {
let result = event.target.result;
// Access the retrieved data
};

Advanced Querying Techniques:

IndexedDB provides various advanced querying techniques such as range queries, compound queries, and filtering based on multiple properties. These techniques involve using methods like openCursor(), openKeyCursor(), and IDBKeyRange. Explaining these techniques in detail goes beyond the scope of this tutorial, but they offer powerful ways to retrieve specific subsets of data from your object store.

Deleting and Updating Data:

Removing Records:
To delete a record from an object store, you can use the delete() method.

let transaction = db.transaction("myObjectStore", "readwrite");
let objectStore = transaction.objectStore("myObjectStore");

let deleteRequest = objectStore.delete(1);

deleteRequest.onsuccess = function(event) {
// Record deleted successfully
};

Updating Records:
To update an existing record in an object store, you can use the put() method.

let transaction = db.transaction("myObjectStore", "readwrite");
let objectStore = transaction.objectStore("myObjectStore");

let updateRequest = objectStore.put({ id: 1, name: "Jane Doe", age: 30 });

updateRequest.onsuccess = function(event) {
// Record updated successfully
};

These examples cover the topics of transactions, querying and filtering data, and updating and deleting records in IndexedDB. The next section will focus on working with cursors, while the following sections will cover handling upgrades and schema changes, as well as best practices and performance optimization.

Working with Cursors:

Introduction to Cursors:
Cursors in IndexedDB provide a way to iterate over a set of records in an object store or index. Cursors allow you to navigate, filter, and modify data efficiently.

Navigating Cursors:
To navigate a cursor, you can use methods such as openCursor(), continue(), and advance(). Here's an example of iterating over all records in an object store using a cursor.

let transaction = db.transaction("myObjectStore", "readonly");
let objectStore = transaction.objectStore("myObjectStore");

let cursorRequest = objectStore.openCursor();

cursorRequest.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
// Access the current record
console.log(cursor.value);

// Move to the next record
cursor.continue();
}
};

Modifying Data with Cursors: Cursors can also be used to modify data. By opening a cursor in a read-write transaction, you can update or delete records. Here’s an example of updating a property of each record using a cursor.

let transaction = db.transaction("myObjectStore", "readwrite");
let objectStore = transaction.objectStore("myObjectStore");

let cursorRequest = objectStore.openCursor();

cursorRequest.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
// Update a property of the current record
cursor.value.age += 1;

// Update the record in the object store
cursor.update(cursor.value);

// Move to the next record
cursor.continue();
}
};

Handling Upgrades and Schema Changes:

Upgrading Database Schema:
When you need to make changes to your database schema, such as adding or removing object stores or indexes, you can handle the upgrade process by listening to the onupgradeneeded event.

let request = indexedDB.open("myDatabase", 2);

request.onupgradeneeded = function(event) {
let db = event.target.result;

// Handle schema changes or upgrades
if (!db.objectStoreNames.contains("newObjectStore")) {
let newObjectStore = db.createObjectStore("newObjectStore", { keyPath: "id" });
newObjectStore.createIndex("nameIndex", "name", { unique: false });
}
};

Migrating Data:
During a schema upgrade, you may need to migrate existing data to match the new schema. You can achieve this by creating a new object store or index, retrieving data from the old store, and adding it to the new one.

request.onupgradeneeded = function(event) {
let db = event.target.result;

if (!db.objectStoreNames.contains("newObjectStore")) {
let newObjectStore = db.createObjectStore("newObjectStore", { keyPath: "id" });
newObjectStore.createIndex("nameIndex", "name", { unique: false });

let oldObjectStore = event.target.transaction.objectStore("oldObjectStore");
let cursorRequest = oldObjectStore.openCursor();

cursorRequest.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
// Migrate data from the old store to the new store
newObjectStore.add(cursor.value);

// Move to the next record
cursor.continue();
}
};
}
};

Best Practices and Performance Optimization:

Using Bulk Operations:
IndexedDB provides bulk operations like put() and delete() that can process multiple records in a single transaction, improving performance. Instead of performing individual operations, you can leverage these bulk methods to efficiently add, update, or delete multiple records.

Index Optimization:
Properly defining and utilizing indexes can significantly enhance query performance. Analyze your data access patterns and create indexes on properties that are frequently used for filtering or sorting.

Managing Connection Lifecycles:
It’s important to manage the lifecycle of IndexedDB connections properly. Open connections when needed and close them when they are no longer required to avoid resource leaks and improve performance. Additionally, handle errors gracefully and implement error handling and recovery mechanisms.

By following these best practices, you can optimize the performance of your IndexedDB-based applications and ensure efficient data management.

In the next section, we’ll explore real-world examples of using IndexedDB, including building a task manager and implementing offline support.

Real-World Example: Building a Task Manager with Offline Support:

Let’s walk through a real-world example of using IndexedDB to build a task manager web application with offline support. The application allows users to create, update, and delete tasks even when they are offline, and automatically syncs the data with the server when an internet connection is available.

Database Setup:
First, we’ll set up the database and object store for storing tasks. We’ll define an object store named “tasks” with an auto-generated key path.

let request = indexedDB.open("TaskManagerDB", 1);

request.onupgradeneeded = function(event) {
let db = event.target.result;

if (!db.objectStoreNames.contains("tasks")) {
let tasksObjectStore = db.createObjectStore("tasks", { autoIncrement: true });
tasksObjectStore.createIndex("statusIndex", "status", { unique: false });
}
};

request.onsuccess = function(event) {
let db = event.target.result;
// Database opened successfully
};

request.onerror = function(event) {
// Error occurred while opening the database
};

Adding Tasks:
Next, we’ll add a function to add a new task to the database. This function will open a transaction, get the object store, and add the task object to the store.

function addTask(task) {
let transaction = db.transaction("tasks", "readwrite");
let tasksObjectStore = transaction.objectStore("tasks");

let addRequest = tasksObjectStore.add(task);

addRequest.onsuccess = function(event) {
// Task added successfully
};

addRequest.onerror = function(event) {
// Error occurred while adding the task
};
}

Retrieving Tasks:
To retrieve tasks from the database, we can use a cursor. Here’s an example of retrieving all tasks and displaying them in the UI.

function displayTasks() {
let transaction = db.transaction("tasks", "readonly");
let tasksObjectStore = transaction.objectStore("tasks");

let cursorRequest = tasksObjectStore.openCursor();

cursorRequest.onsuccess = function(event) {
let cursor = event.target.result;
if (cursor) {
let task = cursor.value;
// Display the task in the UI
console.log(task);

// Move to the next record
cursor.continue();
}
};
}

Updating and Deleting Tasks:
To update or delete a task, we can use the put() and delete() methods of the object store. Here's an example of updating a task's status:

function updateTaskStatus(taskId, newStatus) {
let transaction = db.transaction("tasks", "readwrite");
let tasksObjectStore = transaction.objectStore("tasks");

let getRequest = tasksObjectStore.get(taskId);

getRequest.onsuccess = function(event) {
let task = event.target.result;
if (task) {
task.status = newStatus;
let updateRequest = tasksObjectStore.put(task);

updateRequest.onsuccess = function(event) {
// Task updated successfully
};
}
};
}

Similarly, you can implement a function to delete a task using the delete() method of the object store.

Syncing with the Server:
To sync the tasks with the server when an internet connection is available, you can listen for the online event and trigger a synchronization process. You can use AJAX requests or fetch API to send the tasks to the server and update the server-side database.

window.addEventListener("online", syncWithServer);

function syncWithServer() {
// Retrieve unsynced tasks from the database
// Send tasks to the server using AJAX requests or fetch API
// Update the server-side database

// Once synchronization is complete, you can clear the local database or mark synced tasks
}

By implementing this offline task manager example, you can provide users with a seamless experience for managing tasks, even in offline scenarios.

In the final section, we’ll cover additional resources and references for further learning about IndexedDB.

Additional Resources and Refrences

  1. MDN Web Docs — IndexedDB — The official documentation by Mozilla provides a comprehensive guide to IndexedDB, including detailed explanations, examples, and reference material. You can find it at: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
  2. IndexedDB API — The W3C specification for the IndexedDB API defines the standard interface and functionality of IndexedDB. You can refer to the specification for a deeper understanding of the underlying concepts and methods: https://www.w3.org/TR/IndexedDB/
  3. Using IndexedDB — A tutorial by HTML5 Rocks that covers the basics of IndexedDB, including setting up a database, CRUD operations, querying data, and handling upgrades: https://www.html5rocks.com/en/tutorials/indexeddb/todo/
  4. IndexedDB on Can I use — Can I use is a website that provides browser compatibility information. Check the IndexedDB section to see which browsers support IndexedDB and its features: https://caniuse.com/indexeddb
  5. IndexedDB Examples — The IndexedDB Samples GitHub repository provides a collection of examples demonstrating various aspects of using IndexedDB, including simple CRUD operations, advanced queries, and more: https://github.com/mdn/IndexedDB-examples
  6. IndexedDB Shims and Libraries — There are several libraries and shims available that provide abstractions and simplifications for working with IndexedDB. Examples include Dexie.js (https://dexie.org/) and localForage (https://localforage.github.io/localForage/). These libraries can help streamline your development process and provide additional features and utilities.

By exploring these resources, you’ll gain a solid understanding of IndexedDB and be well-equipped to utilize its features effectively in your web applications.

In the next article, we’ll explore the utilization of cursors in IndexedDB. Cursors offer an efficient mechanism for traversing through extensive datasets. Additionally, we’ll delve into the various types of cursors and their applications in optimizing data retrieval and manipulation processes.

If you find this information valuable, consider following me for more insightful content. Don’t forget to give a round of applause to the story if it resonates with you! Your support is greatly appreciated.

--

--

Amresh Kumar

Software Engineer with 3 years of experience in building full-stack application.