There are a lot of great tutorials that go into a lot of depth on database theory and terminology and allow the power of IndexedDB. If you want that, this is not the right article for you. I’ve literally written a relational database engine (not SQL, but the services that manage data and indices and transactions) and I’ve avoided IndexedDB because I just don’t feel like going through all that detail. I’ll try to describe the minimum you need to do to make it work.
Local Storage
localStorage has worked well for me to store data on a user’s browser until recently:
localStorage.setItem("name","fred");
const name = localStorage.getItem("name");
Nice and easy and does what I expect. It’s easy to store more complex data with JSON stringify and parse.
localStorage.setItem("person-1",
JSON.stringify({id:1, first_name:'fred'}))
localStorage.setItem("person-2",
JSON.stringify({id:2, first_name:'alfred',
"last_name":"christianson"}))
const person1 = JSON.parse(localStorage.getItem("person-1"))
const person2 = JSON.parse(localStorage.getItem("person-2"))
Just wrap setItem() and getItem() with functions to do validation, error checking and whatever else you need and you have pretty powerful database-like functionality. In fact, you created a key-value database and just a few more steps gives you relations.
Any time I needed more it was a job for the server and an API.
My Latest Project
Things changed with my latest project. I have 40,000 photos and data about each one (name, time, size, resolution, tags, etc). I want a fast load, and can’t get that with an API. So I tried saving a copy in localStorage and quickly blew past the size limit (about 5MB). So I decided to finally learn IndexedDB for more data.
Major browsers have dev tools to look at the contents. This is what I have see in Firefox

- The storage tab shows all storage for the host:port
- Indexed DB in the tree shows databases I’ve created. I only have one named “media-tagger”. That database has 3 tables.
- The “media-files” table is highlighted in the tree, so I see all records in that table.
- The first row is highlighted in #3 so I see the object that will be returned when it is requested.
Chrome and Edge are a little different but provide the same details. I assume Safari also provides this.
The Database
Cautions:
- Different browsers may have a name besides indexedDB. I tested chrome and firefox
- It probably won’t work in private mode browsing
Let’s create a real simple database. The only requirement for records is that each object has a field named “id”. For example you can put all of these records in it:
{ "id": 1, "name": "fred"}
{ "id": 2, "food": "banana", "animal": "elephant", "age": 58 }
{ "id": 3}
The first thing we need to do is open the database. If it doesn’t exist, the browser creates it. indexedDB needs 2 arguments: name and version. Start with version 1.
const request = indexedDB.open("indexeddb-example", 1);
This starts an asynchronous operation to create the database the first time and open it after.
Since it’s asynchronous, we need to provide 3 function to get the result
request.onerror = handleDBOpenError;
request.onsuccess = handleDBOpenSuccess;
request.onupgradeneeded = handleUpdgradeNeeded;
onerror(event) is called if something goes wrong. I’ll ignore it here.
onsucces(event) is called when the database has been opened (and created or upgraded if needed). It will look something like this
// need somewhere to save the opened database object.
let theDatabase = null; // null==>not opened yet
function handleDBOpenSuccess(event) {
// database is openened. save it
theDatabase = event.target.result;
}
Now for the part many tutorials over-complicate. When the database is created it calls onupgradeneeded(), so we need to initialize the database:
function handleUpdgradeNeeded(event) {
const database = event.target.result;
database.createObjectStore("example-table", { keyPath: "id" }); };
That’s it:
- We have a database named “indexeddb-example”
- The tatabase has one table named “example-table”. IndexedDB correctly calls it an objectStore but you can think of it as a table if that’s what you’re used to.
We can put any object in the database as long as it has a field named “id”. And we can retrieve any object using the id.
There are a whole lot of other things you can do in the upgrade both when the database is created and when you want to change it (add tables, and index, etc). Find a tutorial that works for you and experiment. For upgrading you will probably need to learn a little more.
If you just want a simple replacement for localStorage, this will do it. You can change keyPath to “name” if you want a name/value store.
Adding Data
We have to create a transaction to add data. If you know what that means, great. If not, don’t worry about it. For the simple case it doesn’t really do anything but it’s required:
const record = {"id": "name", "value": "value.value"fred"};
const transaction =
theDatabase.transaction('example-table','readwrite');
const store = transaction.objectStore('example-table');
store.put(record);
That’s the important part. The “id” field of the record is required for our objectStore. It can be a number or a string. The record can contain any other fields you want. It can be a complicated object with just about anything that would be valid in a JSON object.
There’s more to it:
The transation has callbacks for success and failure. The success callback is called “complete” because the put() doesn’t happen right away and the record is not available until it completes.
You can use add() instead of put(). add() will return an error if a record already exists with the same “id” (keyPath). put() will replace the old value.
You should handle errors and wait for complete and other things. But if you’re just experimenting that’s all you need.
Getting Data
Retrieving data also requires a transaction. But this time we have to wait for the onsuccess callback (we should also handle onerror as we should do with put())
const transaction =
theDatabase.transaction('example-table', 'readonly');
const store = transaction.objectStore('example-table');
const request = store.get(id);
request.onsuccess = (event) => {
const record = event.target.result;
//...
}
This time the transaction is ‘readonly’ though we could use ‘readwrite’ in more complicated scenerios. The id parameter in store.get(id) should be the value of the “id” property of a record you previously put() into the database.
Summary
I tried to provide a minimal explanation and sample code to get started with IndexedDB. You can use it in an application successfully just like this. I glossed over many details, and there is a lot more you can do. It’s best to get a feel for the database here, then dive into the more comprehensive tutorials.
After adding records, use the browser developer tools to see how it looks. Even if you don’t need it while learning, it’s good to know how it works for your real-world application.
Find me on twitter if you have any corrections, comments, or topics you’d like me to write about in the future.