JavaScript - - By Jatin Shridhar

Getting Started with PouchDB Client-Side JavaScript Database

This article was peer reviewed by Sebastian Seitz and Taulant Spahiu. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

Over recent years, client side web applications have gotten more and more sophisticated. Browsers have consistently been providing better JavaScript performance, and are capable of doing more and more things, with rich JavaScript APIs for things like geolocation, and peer-to-peer communication.

The rise of rich web applications also created a need for good client-side storage mechanisms, and that is where JavaScript databases like PouchDB come in.

What is PouchDB?

PouchDB is an open-source JavaScript database inspired by Apache CouchDB that is designed to run well within the browser.

What is a JavaScript database?

In very simple terms, a JavaScript database is a JavaScript object that provides methods (or API) to put, get and search data. In fact, a plain old JavaScript object is the simplest kind of JavaScript database. If you are familiar with Meteor, then you might have heard of Minimongo which is another client side JavaScript database that mimics that MongoDB API.

PouchDB is a JavaScript implementation of CouchDB. Its goal is to emulate the CouchDB API with near-perfect fidelity, while running in the browser or in Node.js.

What makes PouchDB different from databases like Minimongo is that, by default, it is not just in-memory, it uses IndexedDB behind the scenes for its storage. IndexedDB is a low-level API for client-side storage of significant amounts of structured data, including files/blobs. What this means is that PouchDB data is stored on disk, and will be available even after page refresh (however, data stored by one browser will not be available to other browsers).

Different adapters let you change the underlying data storage layer.

Relation to CouchDB

PouchDB is a JavaScript implementation of CouchDB, and emulates it’s API as closely as possible.

In CouchDB, you’d fetch all the documents using this API call

/db/_all_docs?include_docs=true

In PouchDB, it becomes

db.allDocs({include_docs: true})

PouchDB enables applications to store data locally while offline, then synchronize it with CouchDB when the application is back online.

Now, let’s see how you can use PouchDB in your applications.

Installation

To start using PouchDB you just need to include the PouchDB client library. You can use the standalone build, which makes the PouchDB constructor globally available on the window object

<script src="https://cdn.jsdelivr.net/pouchdb/5.4.5/pouchdb.min.js"></script>

or, if you are using it in Node.js/browserify/webpack environment, you can install it with npm.

$ npm install pouchdb --save

Then in your JavaScript:

var PouchDB = require('pouchdb');

(Fun Fact: npm isntall pouchdb also works!)

Working with PouchDB

Creating a database

Creating a PouchDB database is as simple as calling the PouchDB constructor. Let’s create a database called ‘Movies’.

var movies = new PouchDB('Movies');

After running that, you can see basic information about your database, by using the info method, which returns a Promise.

movies
 .info()
 .then(function (info) {
   console.log(info);
 })

The code above outputs the following:

{"doc_count":0,"update_seq":0,"idb_attachment_format":"binary","db_name":"Movies","auto_compaction":false,"adapter":"idb"}

The adapter field indicates that underneath it’s using IndexedDB.

Working with documents

PouchDB is a NoSQL, document-based database, so there is no rigid schema and you can just insert JSON documents directly. Let’s see how you can insert, update, retrieve or delete documents.

Creating a document

You can create a new document using the put method

// returns a promise
db.put(doc, [docId], [docRev], [options])

The params in square brackets are optional. Each document has an _id field associated with it, which works as a unique identifier.

Create a new doc in the previously created Movies database by running the following code:

movies
  .put({
    _id: 'tdkr',
    title: 'The Dark Knight Rises',
    director: 'Christopher Nolan'
  }).then(function (response) {
    console.log("Success", response)
  }).then(function (err) {
    console.log("Error", err)
  })

The response, in case of success, will be something like:

Success {ok: true, id: "tdkr", rev: "3-f8afdea539618c3e8dceb20ba1659d2b"}

Calling movies.info() now will give {doc_count: 1} along with other data indicating that our document has indeed been inserted.

The rev field in the response indicates a revision of document. Each document has a field by the name _rev. Every time a document is updated, the _rev field of document is changed. Each revision point to it’s previous revision. PouchDB maintains a history of each document (much like git).

Read a Document

PouchDB provides a get API method to retrieve a document by its ID. Running:

movies
  .get('tdkr')
  .then(function(doc) {
    console.log(doc)
  })
  .catch(function (err) {
    console.log(err)
  })

will give a response like

{title: "The Dark Knight Rises", director: "Christopher Nolan", _id: "tdkr", _rev: "3-f8afdea539618c3e8dceb20ba1659d2b"}

Update a Document

Let’s say we want to add a ‘year’ field to our document. You’d update the above created doc by running:

movies
  .get('tdkr')
  .then(function(doc) {
    doc.year = "2012"    // new field
    console.log(doc._rev) // doc has a '_rev' field
    return db.put(doc)   // put updated doc, will create new revision
  }).then(function (res) {
    console.log(res)
  })

When updating a document, you must provide a _rev field.

You should see similar output in console:

{ok: true, id: "tdkr", rev: "4-7a34189fb8f2e28fe08b666e699755b8"}

indicating the new revision of document.

Deleting Documents

Deleting a document in PouchDB just sets it’s _deleted property to true. You can call .remove() to do that:

movies
  .get('tdkr')
  .then(function(doc) {
    return movies.remove(doc) // return the promise
  }).then(function(res) {
    console.log("Remove operation response", res)
  })

which is equivalent to doing

movies
  .get('tdkr')
  .then(function (doc) {
    doc._deleted = true
    return db.put(doc)
  })
  .then(...)

Deleting a database

You can delete a database by calling destroy() on the db object.

// returns a promise
movies.destroy() 

Bulk Operations

Until now we have worked with individual documents in PouchDB. It, however, also provides APIs to work with a collection of documents. PouchDB provides two methods for bulk operations – bulkDocs() for bulk writes, and allDocs() for bulk reads.

The bulkDocs() method is pretty simple. It just takes an array of documents that you want to insert into the database.

Insert multiple documents

// Returns a promise
movies.bulkDocs([
  {
    _id: 'easy-a',
    title: "Easy A",
    // other attribues
  },
  {
    _id: 'black-swan',
    title: 'Black Swan',
    // ...
  }
])

Sample Response:

[
  {
    "ok": true,
    "id": "easy-a",
    "rev": "1-84abc2a942007bee7cf55007cba56198"
  },
  {
    "ok": true,
    "id": "black-swan",
    "rev": "1-7b80fc50b6af7a905f368670429a757e"
  }
]

If you want to insert multiple documents, using the bulk API is generally a better way than doing multiple put() requests. Bulk operations tend to be faster than individual operations, because they can be combined into a single transaction (for a local IndexedDB/WebSQL store) or a single HTTP request (for a remote CouchDB server).

Retrieve Multiple Documents

To read multiple docs, PouchDB provides the allDocs() method.

// without {include_docs: true}, only document ids are returned
movies
  .allDocs({include_docs: true})
  .then(function (docs) {
    console.log(docs)
  })

It’s a fast and very useful method. From the PouchDB documentation:

allDocs() is the unsung star of the PouchDB world. It not only returns documents in order – it also allows you to reverse the order, filter by _id, slice and dice using “greater than” and “less than” operations on the _id, and much more.

By default, the documents are returned in ascending _id order. You can specify {descending: true} to reverse the order.

movies
  .allDocs({
    include_docs: true, 
    descending: true
  })
  .then(...)

You can also specify a startkey and endkey parameter to get documents within a range. For example, to get all the movies whose _id starts with ‘a’, or ‘b’, you could run this query:

movies
  .allDocs({
    include_docs: true,
    startkey: 'a',
    endkey: 'c'
  })
  .then(console.log)
  .catch(console.log)

The startKey and endKey parameters are particularly useful for paginated APIs.

Go Realtime with ChangeFeeds

We talked about how PouchDB uses the _rev field to keep track of revisions, with each revision pointing to the previous revision. PouchDB and CouchDB use this chain of revisions for database replication.

However, an implication of this replication algorithm is that it allows you to see the history of the database, letting you answer questions like

  • What changes have been made to the database since a given time?
  • What changes have been made to a particular document?

This is where the changes() API comes in.

To fetch all changes since the beginning of time:

db.changes({
  since: 0,
  include_docs: true
}).then(function (changes) {
  console.log(changes)
}).catch(...)

However, in a web application you are generally more interested in seeing the changes to database that occur after initial page load, so that you can change the UI accordingly. The PouchDB/CouchDB duo have got you covered there also with the live change feed.

db
  .changes({
    since: 'now',
    live: true,
    include_docs: true
  })
  .on('change', function (change) {
    // This is where you can modify UI, based on database change.
    // change.id contains the doc id, change.doc contains the doc
    if (change.deleted) {
      // document was deleted
    } else {
      // document was added/modified
    }
  })
  .on('error', function (err) {
    // handle errors
  })

So, if you had, say, a basic list application, then you could add items in one window, and they’d show up in another window in real-time.

You can see a demo of this in action.

Sync: Take PouchDB Data Beyond Browser

Most apps will need to store data on the back-end and not just in the browser, so you can use PouchDB to save your data locally, but sync it with a back-end CouchDB instance so that the data is available anywhere and not just in that particular browser.

PouchDB provides a very simple API to do this. Assuming, you have a remote CouchDB database set up, syncing it just a matter of two lines of JavaScript.

// local database, that lives in the browser's IndexedDB store
var localDB = new PouchDB('mylocaldb')

// remote CouchDB 
var remoteDB = new PouchDB('http://localhost:5984/myremotedb')

You can replicate local changes to a remote DB by writing

localDB
  .replicate
  .to(remoteDB)
  .on('complete', function () {
    // local changes replicated to remote
  }).on('error', function (err) {
    // error while replicating
  })

However, since many users might have access to same database, it’s more useful to be able to sync changes from a remote DB to the browser. PouchDB has got you covered there also.

The bidirectional sync can be achieved using this one line of JavaScript:

// replicates once
localDB.sync(remoteDB);

Or for live sync:

// keeps syncing changes as they occur
localDB.sync(remoteDB, {live: true})

Next Steps

PouchDB also has a growing ecosystem of plugins and framework adaptors that we don’t have the space to go into here, but are definitely worth checking out. Also, while working with PouchDB, you can use PouchDB Inspector chrome extension, which provides a nice GUI to see your database.

That’s it for this introductory look at PouchDB. It’s definitely one of the more interesting databases out there, and I hope you can see how you could use it to build offline-first, real-time applications.

Sponsors
Login or Create Account to Comment
Login Create Account