Using Dexie.js to write slick IndexedDB code
Tags: javascript, codenewbie, webdev, indexeddb
Published: 2020-11-28 16:36:10

---
title: Using Dexie.js to write slick IndexedDB code
published: true
description: Part 4 of my IndexedDB tutorial series, on the basic IndexedDB actions
tags: #javascript #codenewbie #webdev #indexeddb
---

In 2018 and 2019, I wrote a three-part series on how to make a small web app in IndexedDB, which you can see at the links below:

* [Part 1: Build a basic app with IndexedDB](https://dev.to/andyhaskell/build-a-basic-web-app-with-indexeddb-38ef)
* [Part 2: Testing IndexedDB code with Jest](https://dev.to/andyhaskell/testing-your-indexeddb-code-with-jest-2o17)
* [Part 3: Using promises in IndexedDB code](https://dev.to/andyhaskell/using-promises-in-indexeddb-4nc0)

Recently, I have tried out a popular library called [Dexie.js](dexie.org). I found that it really makes IndexedDB code a lot more straightforward and fast to write with a beautiful abstraction over the built-in IndexedDB API, so I'd like to show you how you would re-create the database layer of the app from my previous tutorial using Dexie!

If you haven't read my other tutorials, reading them helps but isn't strictly necessary for following along in this one, so no need to backtrack. But as we'll discuss at the end, [core IndexedDB concepts]((https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API/Basic_Concepts_Behind_IndexedDB)) are still worth knowing if you're working with Dexie, as Dexie is an abstraction over IndexedDB.

All the code from this tutorial can be found on GitHub [here](https://github.com/andyhaskell/indexeddb-tutorial/tree/master/4-dexie-tutorial).

## Review of our app and its database interactions

The app we were making is a sticky note app, where you can write sticky notes and display them in forward or reverse chronological order. So the database interactions we had to implement are:

* 🏗️ Set up the database, creating an IndexedDB **object store** for our sticky notes, with an **index** on the timestamp of storing it
* 📝 Adding a sticky note to the object store
* 🚚 Retrieving our sticky notes, in forward or reverse order so we can display them

The app looks like this:

![Sample screenshot of the web app from the tutorial, with a text box for adding messages to a list, and a list for displaying the messages in the database](https://raw.githubusercontent.com/andyhaskell/indexeddb-tutorial/master/sample.png)

## Making our skeleton Dexie class

Let's start by making a file called `db.js`. When I make the database layer of something, I prefer to wrap the logic of all database interactions in a single class so it's all in one place. Here's what a skeleton of that class will look like:

```javascript
let { Dexie } = require('dexie');

// Database handles all database interactions for the web app.
class Database extends Dexie {
  // our Database constructor sets up an IndexedDB database with a
  // sticky notes object store, titled "notes".
  constructor() {}

  // addStickyNote makes a sticky note object from the text passed
  // in and stores it in the database. Returns a promise that
  // resolves on success.
  addStickyNote(message) {}

  // getNotes retrieves all sticky notes from the IndexedDB
  // database, in forward or reverse chronological order. Returns
  // a promise that resolves on success, containing our array of
  // sticky notes.
  getNotes(reverseOrder) {}
}

module.exports = Database;
```

As you can see, we have a class with three methods: a constructor for setting up the database with a sticky notes object store, `addStickyNote` for storing a sticky note in the notes object store, and `getNotes` for retrieving the sticky notes.

Even just from the skeleton class, we already can notice a couple things about Dexie:

```javascript
class Database extends Dexie {
  constructor() {}

  // more code below
}
```

First of all, I made the class extend the `Dexie` class. `Dexie` is the main class of the database library, and it [represents a connection](https://dexie.org/docs/Dexie/Dexie) to an IndexedDB database.

```javascript
  // addStickyNote makes a sticky note object from the text passed
  // in and stores it in the database. Returns a promise that
  // resolves on success.
  addStickyNote(message) {}
```

The other thing worth noticing is that I had both the `addStickyNote` and `getNotes` methods return promises. In [part 3](https://dev.to/andyhaskell/using-promises-in-indexeddb-4nc0) of this series, we put a fair amount of effort into wrapping IndexedDB's callback API in a promise-based abstraction to make it easier to work with. In Dexie, all the database interactions return promises, and that means out of the box, they work well with `async/await` patterns.

## Writing a database constructor

Just like with setting up a database in plain IndexedDB, in our database constructor we want to create the database, give it an object store, and define indices on that store. Here's what that would look like with Dexie:

```javascript
constructor() {
  super('my_db');

  this.version(1).stores({
    notes: '++id,timestamp',
  });

  this.notes = this.table('notes');
} 
```

Just three statements to make everything, and unlike in the `setupDB` function from the previous tutorials, we aren't thinking at all about IndexedDB "open DB" requests, or `onupgradeneeded` callbacks. Dexie handles all that logic for us behind the scenes! Let's take a look at what each statement does:

```javascript
super('my_db');
```

In the first statement, we run the `Dexie` constructor, passing in the name of our database. By doing this, we now have a database created with the name "my_db".

```javascript
this.version(1).stores({
  notes: '++id,timestamp',
});
```

In the second statement, we get version `1` of the database schema with the [version method](https://dexie.org/docs/Dexie/Dexie.version()), and then make our object stores using the [stores method](https://dexie.org/docs/Version/Version.stores()).

The object we pass into `stores` defines the object stores we want to make; there's one store made for each key in that object, so we have a `notes` store made with the `notes` key.

We define the indices on each store using the comma-separated string values on the object:
* The `++id` string makes the ID of a sticky note the object store's **auto-incrementing primary key**, similar to passing `{ autoIncrement: true }` into the built-in IndexedDB `createObjectStore` method.
* We also make an index on `timestamp` so we can query for sticky notes in chronological order.

You can see the other syntax for making indices for your IndexedDB tables in the documentation for the [Version.stores](https://dexie.org/docs/Version/Version.stores()) method.

```javascript
this.notes = this.table('notes');
```

Finally, totally optionally, we can use the [Dexie.table](https://dexie.org/docs/Dexie/Dexie.table()) method to get a Dexie `Table` object, which is a class that represents our object store. This way, we can do interactions with the `notes` object store using methods like `this.notes.add()`. I like doing that to have the database table represented as a field on the class, especially if I'm using TypeScript.

We've got our database constructor, so now we've got a big implementation of `addNotes` to write.

## Adding a sticky note to the database in Dexie

In the built-in IndexedDB API, adding an item to an object store would involve:

1. Starting a `readwrite` transaction on the `notes` object store so no other interactions with that store can happen at the same time, and then retrieving our object store with `IDBTransaction.objectStore`.
2. Calling `IDBObjectStore.add` to get an IndexedDB **request** to add the sticky note.
3. Waiting for that to succeed with the request's `onsuccess` callback.

Let's see what that all looks like in Dexie:

```javascript
addStickyNote(message) {
  return this.notes.add({ text: message, timestamp: new Date() });
}
```

Just a single statement of code, and we didn't need to think about IndexedDB transactions or requests because when we call [Table.add](https://dexie.org/docs/Table/Table.add()), Dexie handles starting the transaction and making the request behind the scenes!

`Table.add` returns a promise that resolves when the underlying IndexedDB request succeeds, so that means in our web app, we can use promise chaining or the async/await pattern like this:

```javascript
function submitNote() {
  let message = document.getElementById('newmessage');
  db.addStickyNote(message.value).then(getAndDisplayNotes);
  message.value = '';
}
```

we put `getAndDisplayNotes` in the function that we run as the `then` of the promise that `addStickyNote` returns.

By the way, while `Table.add` does abstract away transactions, that's not to say IndexedDB transactions can't be more explicitly created in Dexie when we need them. If we want to do something like store items in two object stores at the same time, we could use the [Dexie.transaction](https://dexie.org/docs/Dexie/Dexie.transaction()) method.

Now let's see how we can query for sticky notes from our object store!

## Retrieving sticky notes

In the built-in IndexedDB API, if we wanted to retrieve all the items from our `notes` object store, we would do the following:

1. Start a `readonly` transaction on our `notes` object store.
2. Retrieve the object store with `IDBTransaction.getObjectStore`.
3. Open a cursor for our query we want to make.
4. Iterate over each item in the store that matches our query.

With Dexie, we can do this querying in just one statement that's got a slick chaining API!

```javascript
getNotes(reverseOrder) {
  return reverseOrder ?
    this.notes.orderBy('timestamp').reverse().toArray() :
    this.notes.orderBy('timestamp').toArray();
}
```

Let's break this down:

* We select which index we want to sort results with using [Table.orderBy](https://dexie.org/docs/Table/Table.orderBy()); in this case we want to order our results by their timetsamps.
* If `reverseOrder` is true, then we can use the [Collection.reverse](https://dexie.org/docs/Collection/Collection.reverse()) method, so we get the newest sticky notes first.
* Finally, `toArray` returns a promise that resolves when our query is successfully run. In the promise's `then` method, you can then make use of our array of sticky notes.

That's not even close to all the ways you can modify a query with Dexie though. Let's say we only wanted sticky notes that are:

* made in the past hour
* newest ones first
* and a maximum of five of them

Here's how we would chain that query:

```javascript
let anHourAgo = new Date(Date.now() - 60 * 60 * 1000);

return this.notes
  .where('timestamp')
    .above(anHourAgo)
  .orderBy('timestamp')
  .reverse()
  .limit(5)
  .toArray();
```

With all our methods made, we have our first Dexie database class written!

## Dexie users should still learn about the built-in IndexedDB API's core concepts

As you can see from this tutorial, Dexie.js provides a beautiful abstraction over IndexedDB requests and transactions, taking a lot of event callback management out of the work you do with an IndexedDB database. I personally find Dexie to be a really satisfying API to use because of the simplicity it brings.

If this is your first experience with IndexedDB, though, it is still worth being familiar with the core concepts this technology. Ultimately, all of the functionality of Dexie is built on top of the built-in IndexedDB API, so that means that how IndexedDB works ultimately influences how Dexie works. Some of these concepts I find important to know about are:

* In IndexedDB, databases are composed of **object stores**, and you make **indices** on those object stores to make it more efficient to query for data by certain object fields. And as we saw, object stores and indices are a big part of Dexie as well.
* IndexedDB is a **noSQL** database, so while it has indices and the ability to make complex queries, since the database isn't relational like Postgres, you can't do joins between tables. So if you want to retrieve two kinds of data together, you'll want to design your object stores and indices around storing those kinds of data together.
* All IndexedDB interactions are **asynchronous** and work with the [event loop](https://medium.com/samsung-internet-dev/promises-promises-c91d454aea11) to not block the JS runtime while running requests. This is why in the built-in API we get the results of requests with callbacks, while Dexie uses promises.
* You can take a closer look at your databases and the stored data in your browser by going do **Developer Tools > Application > IndexedDB**, and since Dexie is built on top of IndexedDB, you can still get that same convenient panel for debugging your apps!