Skip to content

HTML5 - Web SQL Database

HTML5 - Client-Side Storage with IndexedDB

Section titled “HTML5 - Client-Side Storage with IndexedDB”

Modern web applications often need to store data directly on the user’s computer. While HTML5 introduced features like LocalStorage and SessionStorage for simple key-value pairs, a more robust solution is needed for larger amounts of structured data, offline capabilities, and complex queries. Historically, an API called Web SQL Database was proposed, but it was never formally standardized and is now deprecated due to lack of support across browsers and potential security concerns. It should not be used in modern development.

The current standard for powerful client-side storage is the IndexedDB API. It provides a transactional, asynchronous, object-oriented database within the browser. Unlike Web SQL, it doesn’t use SQL queries. Instead, you work with ‘object stores’ (similar to tables) and ‘indexes’ to efficiently retrieve data.

IndexedDB is ideal for: storing significant amounts of user data, caching application data for offline use (Progressive Web Apps - PWAs), and managing data that requires querying or indexing. It is supported by all modern web browsers.

Understanding IndexedDB involves grasping these key concepts:

  • Asynchronous Operations: Nearly all IndexedDB operations are asynchronous. They return immediately and notify you of completion (success or error) later via events or Promises. This prevents blocking the main browser thread, ensuring a responsive user interface.
  • Databases: You first open a named and versioned database.
  • Object Stores: Inside a database, you create object stores (like tables in SQL) to hold your data. Each store typically holds objects of a similar type (e.g., ‘users’, ‘products’).
  • Indexes: You can create indexes on specific properties within your objects in an object store. Indexes allow for fast lookups and iteration based on that property (e.g., index on ‘email’ in the ‘users’ store).
  • Transactions: All data operations (reading, writing, deleting) must happen within a transaction. Transactions ensure data integrity – either all operations within the transaction succeed, or none of them do (atomic). Transactions have modes, typically readonly or readwrite.
  • Requests: Asynchronous operations generate request objects (IDBRequest or IDBOpenDBRequest). You attach event handlers (onsuccess, onerror) or use Promises to handle the outcome of these requests.

The first step is to request opening a database using indexedDB.open(). This method is asynchronous.

// Request opening 'myDatabase', version 1
const request = indexedDB.open('myDatabase', 1);
let db;
// Error handler
request.onerror = (event) => {
console.error(`Database error: ${event.target.errorCode}`);
};
// Success handler
request.onsuccess = (event) => {
db = event.target.result;
console.log('Database opened successfully.');
// Database is ready for transactions
};
// Upgrade handler (or initial setup)
request.onupgradeneeded = (event) => {
db = event.target.result;
console.log('Database upgrade needed or initial setup.');
// Create an object store 'users' if it doesn't exist
if (!db.objectStoreNames.contains('users')) {
// Use 'id' as the key path and auto-increment it
const userStore = db.createObjectStore('users', { keyPath: 'id', autoIncrement: true });
// Create an index on the 'email' property for quick lookups
userStore.createIndex('email', 'email', { unique: true });
console.log('Object store "users" created.');
}
// Add other object stores or indexes here if needed for this version
};

Key points about indexedDB.open(name, version):

  • name: The name of your database.
  • version: An integer version number. If the database doesn’t exist or the provided version is higher than the existing one, the onupgradeneeded event fires.
  • onupgradeneeded: This is the only place where you can modify the database structure (create/delete object stores, create/delete indexes). It runs before onsuccess if an upgrade is needed.
  • onsuccess: Fires when the database is successfully opened with the specified version. The event.target.result provides the database connection object (IDBDatabase).
  • onerror: Fires if there’s an error opening the database (e.g., disk quota exceeded, security errors).

All data reading and writing must occur inside a transaction. You start a transaction on the database connection object.

// Assume 'db' is the opened database connection from the previous example
// Start a read-write transaction on the 'users' object store
const transaction = db.transaction(['users'], 'readwrite');
// Get the object store reference from the transaction
const userStore = transaction.objectStore('users');
// Now you can perform operations like add, put, get, delete on userStore
// Transaction completion handlers
transaction.oncomplete = () => {
console.log('Transaction completed successfully.');
};
transaction.onerror = (event) => {
console.error(`Transaction error: ${event.target.error}`);
};
// Example: Add data (see next section)

The db.transaction() method takes two arguments:

  • stores: An array of object store names involved in the transaction (e.g., ['users'] or ['users', 'products']).
  • mode: The transaction mode: 'readonly' (default, for reading data) or 'readwrite' (for adding, updating, or deleting data).

Modern JavaScript often uses Promises or async/await to manage IndexedDB’s asynchronicity, making the code cleaner. While the examples here use event handlers for clarity on the underlying mechanism, wrapper libraries or careful Promise wrapping are common in real projects. You can find resources on this in the MDN documentation.

To add or update data, use the put() or add() methods on the object store within a readwrite transaction.

// Assume 'db' is the opened database connection
const newUser = { name: 'John Doe', email: 'john.doe@example.com' }; // 'id' will be auto-generated
const transaction = db.transaction(['users'], 'readwrite');
const userStore = transaction.objectStore('users');
// Use add() to insert - fails if a user with the same key already exists
// const request = userStore.add(newUser);
// Use put() to insert or update - overwrites if key exists
const request = userStore.put(newUser);
request.onsuccess = (event) => {
// event.target.result contains the key of the added/updated record (the auto-generated id)
console.log(`User added/updated with ID: ${event.target.result}`);
};
request.onerror = (event) => {
console.error(`Error adding/updating user: ${event.target.error}`);
// If using add() and the email index is unique, this might fire on duplicate email
};
// Transaction handlers (oncomplete, onerror) are important too!

Key differences:

  • add(data): Adds the data object. If an object with the same key already exists, the request fails (onerror fires).
  • put(data): Adds the data object or updates an existing object if one with the same key already exists.

To read data, you typically use get() for single records or openCursor() to iterate over multiple records within a readonly (or readwrite) transaction.

Example: Reading a single record by its key:

// Assume 'db' is the opened database connection and user with ID 1 exists
const transaction = db.transaction(['users'], 'readonly');
const userStore = transaction.objectStore('users');
const keyToGet = 1; // The primary key (id) we want to retrieve
const request = userStore.get(keyToGet);
request.onsuccess = (event) => {
const user = event.target.result;
if (user) {
console.log('User found:', user); // { id: 1, name: 'John Doe', email: '...' }
// Update UI, etc.
} else {
console.log(`User with key ${keyToGet} not found.`);
}
};
request.onerror = (event) => {
console.error(`Error getting user: ${event.target.error}`);
};

Example: Reading multiple records using a cursor:

// Assume 'db' is the opened database connection
const transaction = db.transaction(['users'], 'readonly');
const userStore = transaction.objectStore('users');
const users = []; // Array to hold the results
const cursorRequest = userStore.openCursor();
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
// cursor.key contains the key
// cursor.value contains the object
console.log(`Found user: ID=${cursor.key}, Name=${cursor.value.name}`);
users.push(cursor.value);
cursor.continue(); // Move to the next record
} else {
// No more entries
console.log('Finished iterating through users.');
console.log('All users:', users);
// Update UI with the 'users' array
}
};
cursorRequest.onerror = (event) => {
console.error(`Error opening cursor: ${event.target.error}`);
};

You can also use openCursor() on an index to iterate records based on the indexed property’s order or within a specific range.

Here’s a basic HTML page demonstrating opening a database, adding data, and then reading and displaying it:

<!DOCTYPE HTML>
<html>
<head>
<title>IndexedDB Example</title>
<style>
body { font-family: sans-serif; }
#status, #output { margin-top: 10px; padding: 10px; border: 1px solid #ccc; background-color: #f9f9f9; }
#output p { margin: 5px 0; }
</style>
</head>
<body>
<h1>IndexedDB Simple Demo</h1>
<div id="status">Connecting to database...</div>
<div id="output">
<h2>Stored Users:</h2>
<p>Loading...</p>
</div>
<script>
const dbName = 'myDemoDB';
const dbVersion = 1;
let db;
const statusDiv = document.getElementById('status');
const outputDiv = document.getElementById('output');
function logStatus(message) {
console.log(message);
statusDiv.textContent = message;
}
function displayError(message, error) {
console.error(message, error);
statusDiv.textContent = `${message} - See console for details.`;
statusDiv.style.color = 'red';
}
// 1. Open Database Request
const request = indexedDB.open(dbName, dbVersion);
request.onerror = (event) => {
displayError(`Database error: ${event.target.errorCode}`);
};
request.onupgradeneeded = (event) => {
logStatus('Database upgrade needed or initial setup.');
db = event.target.result;
if (!db.objectStoreNames.contains('contacts')) {
const store = db.createObjectStore('contacts', { keyPath: 'id', autoIncrement: true });
store.createIndex('emailIdx', 'email', { unique: true });
logStatus('Object store "contacts" created.');
} else {
logStatus('Object store "contacts" already exists.');
}
};
request.onsuccess = (event) => {
db = event.target.result;
logStatus('Database opened successfully.');
// Add some initial data if needed, then display
addInitialData();
};
function addInitialData() {
const transaction = db.transaction(['contacts'], 'readwrite');
const store = transaction.objectStore('contacts');
let itemsAdded = 0;
const initialContacts = [
{ name: 'Alice', email: 'alice@example.com' },
{ name: 'Bob', email: 'bob@example.com' }
];
initialContacts.forEach(contact => {
// Using put - won't error if they already exist from a previous run
const req = store.put(contact);
req.onsuccess = () => { itemsAdded++; };
req.onerror = (e) => { console.warn(`Could not add ${contact.email}: ${e.target.error}`); };
});
transaction.oncomplete = () => {
logStatus(`Initial data check/addition complete (${itemsAdded} potentially new).`);
displayContacts(); // Now display the data
};
transaction.onerror = (event) => {
displayError('Error during initial data transaction.', event.target.error);
};
}
function displayContacts() {
const transaction = db.transaction(['contacts'], 'readonly');
const store = transaction.objectStore('contacts');
const cursorRequest = store.openCursor();
outputDiv.innerHTML = '<h2>Stored Users:</h2>'; // Clear previous content
let foundData = false;
cursorRequest.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
foundData = true;
const contact = cursor.value;
const p = document.createElement('p');
p.textContent = `ID: ${contact.id}, Name: ${contact.name}, Email: ${contact.email}`;
outputDiv.appendChild(p);
cursor.continue();
} else {
logStatus('Finished loading contacts.');
if (!foundData) {
const p = document.createElement('p');
p.textContent = 'No contacts found in the database.';
outputDiv.appendChild(p);
}
}
};
cursorRequest.onerror = (event) => {
displayError('Error reading contacts.', event.target.error);
outputDiv.innerHTML += '<p style="color: red;">Error loading contacts.</p>';
};
}
</script>
</body>
</html>

This example demonstrates the fundamental asynchronous flow: open -> upgrade/create store (if needed) -> success -> perform transactions (add/put) -> perform transaction (read/cursor) -> display results. Remember to always include robust error handling (onerror) for all requests and transactions.

IndexedDB is a powerful API with more features like using indexes for efficient queries, deleting data, handling version changes gracefully, and understanding storage limits. For more in-depth information, consult the MDN Web Docs:

While the native API is powerful, some developers find its event-based nature verbose. Libraries like Dexie.js (https://dexie.org/) provide a Promise-based wrapper that can significantly simplify working with IndexedDB, especially for complex applications.