March 2026

Using IndexedDB to Store Key Value Pairs with Vanilla JavaScript

I'm working on a Magic Deck Buildermagic. There's about 30,000 cards to choose from. Keeping track of them is beyond the capabilities of localStoragels. IndexedDBidb, on the other hand, has no problem hanging on to that much stuff.

An Actual Solution

Most documentation and tutorials on IndexedDB are disapointing. They show how to store values just fine. They fail to demonstrate how to pull data out in a useful way. The only thing they give you is how to print to console.log(). Nothing about how to, you know, actually use the data outside the functions that query itresults.

The examples also lock in on storing full objects. That's great and all, but I want to use IndexedDB as a simple key/value storekv.

This is how I'm doing that:

JavaScript

const DB_NAME = "example_db";
const STORE_NAME = "storage";
const DB_VERSION = 1;

async function initDB() {
  return new Promise((resolve, reject) => {
    const request = indexedDB.open(DB_NAME, DB_VERSION);
    request.onsuccess = () => resolve(request.result);
    request.onerror= () => reject(request.result);
    request.onupgradeneeded = (event) => {
      const db = event.target.result;
      if (!db.objectStoreNames.contains(STORE_NAME)) {
        db.createObjectStore(STORE_NAME);
      }
    }
  });
}

async function putValue(value, key) {
  const db = await initDB();
  return new Promise((resolve, reject) => {
    const store = db
      .transaction(STORE_NAME, "readwrite")
      .objectStore(STORE_NAME);
    const request = store.put(value, key);
    request.onsuccess = () => resolve(request.result);
    request.onerror= () => reject(request.result);
  });
}

async function getValue(key) {
  const db = await initDB();
  return new Promise((resolve, reject) => {
    const store = db
      .transaction(STORE_NAME, "readonly")
      .objectStore(STORE_NAME);
    const request = store.get(key);
    request.onsuccess = () => resolve(request.result);
    request.onerror= () => reject(request.result);
  });
}

async function deleteValue(key) {
  const db = await initDB();
  return new Promise((resolve, reject) => {
    const store = db
      .transaction(STORE_NAME, "readwrite")
      .objectStore(STORE_NAME);
    const request = store.delete(key);
    request.onsuccess = () => resolve(request.result);
    request.onerror= () => reject(request.result);
  });
}

await putValue("this is alfa", "alfa");
const afterPut = await getValue("alfa");
const afterPutEl = document.querySelector("#afterPut");
afterPutEl.innerHTML = afterPut;

await deleteValue("alfa");
const afterDelete = await getValue("alfa");
const afterDeleteEl = document.querySelector("#afterDelete");
afterDeleteEl.innerHTML = afterDelete;

HTML

<div id="afterPut">--</div>
<div id="afterDelete">--</div>

Output

--
--

Bulk Storage

That's a "non-trivial amount of code". It's also async/await based which adds to the complexity of using it. It would be nice if it was easier to work with, but the flexibility makes the complexity worth it.

-a

end of line

Endnotes

Using localStorage to do the same thing looks like this:

localStorage.setItem("bravo", "this is bravo");
const afterSet = localStorage.getItem("bravo");
const afterSetEl = document.querySelector("#afterSet");
afterSetEl.innerHTML = afterSet;

localStorage.removeItem("bravo");
const afterRemove = localStorage.getItem("bravo");
const afterRemoveEl = document.querySelector("#afterRemove");
afterRemoveEl.innerHTML = afterRemove;

Less than ten lines compared to more than sixty with no async to worry about is nice. It just falls over when trying to store large amounts of data since browsers have much lower limits on localStorage vs IndexedDB.

Normally, you'd check the return values in both the IndexedDB and localStorage approaches before trying to use them. I've skipped both here to reduce the code.

I'm using store.put(value, key) to add the values to the database. This is an upsert style change that updates the value if it exists and adds it if it doesn't. store.add(value, key) can be used to do an add without the update functionality.

The AI search results have the same problem with only showing how to log data instead of using it. When prompted for how to actually use the data they point to external libraries. I expect those libraries offer a lot more than what I've got here. I don't need anything else and would rather avoid the dependency.

References

Footnotes

Very much a Work-In-Progress at press time. It's a lot like Archidekt. Just refined down only the feature I want (and a way to kick the tires for bitty.

The traditional way to store data for a web site when you need something more than a cookie. Great for simple key/value pairs when the values are strings and you don't have too many of them. Storing objects requires jumping through JSON.stringify and JSON.parse. There are stricter size limits than IndexedDB as well.

The new hotness in web app data storage. Just gotta jump through some more hoops.

Using results from IndexedDB without async/await promises moves you into the world of callbacks. I could probably remember how do do that if I tried. I'd rather not.

The general IndexedDB tutorials show how to store objects with IDs. The requires putting the ID in the object. Great when you need it. More complicated than necessary when all you need is a key/value.