IndexedDB
Overview
IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files and blobs.
- Large-scale NoSQL database in the browser
- Data is stored as key-value pairs in object stores
- Supports transactions (mandatory for all read/write operations)
- Accessible in window, service workers, and web workers
When to use IndexedDB
In PWA (Progressive Web App) architecture:
- URL-addressable resources (HTML, CSS, JS, images) — use the Cache API
- Everything else (application data, user state) — use IndexedDB
For simple key-value data, localStorage is simpler. Use IndexedDB when you need structured data, large storage, or offline support.
Terminology
- Database — contains object stores, which contain data. Typically one DB per app.
- Object store — similar to tables in a relational DB. Data is stored as JS objects (no strict schema).
- Index — used to organize data in an object store by a specific property (enables efficient lookups).
- Transaction — same concept as in SQL. Mandatory in IndexedDB for all read/write operations (ensures thread safety).
- Cursor — mechanism to iterate over multiple records in a DB.
idb library
The raw IndexedDB API is not very developer-friendly. Use a wrapper library.
idb — Jake Archibald's IndexedDB Promised library. Thin, promise-based wrapper written in TypeScript. https://github.com/jakearchibald/idb
Define DB schema
Every property in the schema is an object store:
value— the type storedkey— the key typeindexes— index name and data value type
interface IListDB extends DBSchema {
items: {
value: {
id: string;
description: string;
completed: boolean;
};
key: string;
indexes: { 'by-completed': boolean };
};
}
Open and migrate DB
Open the database and run upgrade logic when the version changes:
const db = await openDB<IListDB>('list-db', VERSION, {
upgrade(db) {
if (db.objectStoreNames.contains('items')) {
db.deleteObjectStore('items');
}
const itemsStore = db.createObjectStore('items', {
keyPath: 'id',
});
itemsStore.createIndex('by-completed', 'completed');
console.log("DB upgraded!");
},
});
Save data
data.forEach(async item => {
await db.put('items', item);
});
Read/write with transactions
Normally read/write operations are wrapped in a transaction:
const tx = db.transaction('items', 'readwrite');
const store = tx.objectStore('items');
const val = (await store.get('some guid'));
await store.put({...val!, completed: true});
await tx.done;
NB! Do not await other things inside a transaction — the transaction auto-closes when there are no pending requests, so awaiting unrelated promises will cause it to fail.
Single commands (shortcuts)
It's common to use single commands instead of explicit transactions, so shortcuts are provided by the idb library:
get,getKey,getAll,getAllKeys,count,put,add,delete,cleargetFromIndex,getKeyFromIndex,getAllFromIndex,getAllKeysFromIndex,countFromIndex
Defence Preparation
Be prepared to explain topics like these:
- How does IndexedDB differ from
localStorage? When would you choose IndexedDB? —localStoragestores only strings with a ~5MB limit and has a synchronous, blocking API. IndexedDB is a full NoSQL database that stores structured data (objects, arrays, files, blobs) with much larger storage limits (hundreds of MB). It has an asynchronous API that does not block the main thread. Use IndexedDB when you need to store large amounts of structured data, perform indexed queries, or work with offline-capable applications. - Why are transactions mandatory in IndexedDB? What happens if you
awaitsomething unrelated inside a transaction? — Transactions ensure data consistency — all operations within a transaction either succeed together or fail together (atomicity). IndexedDB auto-commits a transaction when there are no pending requests on it. If youawaitan unrelated promise (like afetchcall) inside a transaction, the transaction sees no pending requests during that wait and auto-closes, causing subsequent operations to fail with an "inactive transaction" error. - What is an object store, and how do indexes help with querying? — An object store is the equivalent of a table in a relational database — it holds records as key-value pairs. Each record has a primary key (auto-generated or from a key path). Indexes are secondary lookup structures that let you query records by properties other than the primary key. Without an index, you would need to iterate over all records to find matches by a non-key field.
- How does database versioning and migration work in IndexedDB? — Each IndexedDB database has a version number. When you open a database with a higher version than what exists, the
upgradecallback runs, where you can create or delete object stores and indexes. This is the only place where schema changes are allowed. Theidblibrary makes this cleaner by mapping version numbers to migration functions that run sequentially. - What does the
idblibrary provide over the native IndexedDB API? — The native IndexedDB API is callback-based and verbose, using event handlers (onsuccess,onerror) on request objects. Theidblibrary wraps this with a promise-based API, so you can useasync/awaitfor all operations. It also provides shortcut methods (db.get(),db.put(), etc.) that handle the transaction automatically for single-command operations, reducing boilerplate significantly.