CommonJS modules in CouchDB

You may not realise that CouchDB actually supports CommonJS modules in design documents. This can be very useful when you want to share code across different list, show, update or validation functions.

Its also a useful way to share code between the client and server by using a CommonJS implementation for the browser. Normally you would store client-side code in an attachment, but by storing it on the design doc you can access it server-side too!

CommonJS modules are a neat way to write JavaScript which doesn't pollute the global namespace. If you're used to using a server-side JavaScript platform (such as node.js) you'll be familiar with the advantages, and may even be able to re-use a few modules.

Adding a CommonJS module

To add a module to a design document you need to store it as a string (much as you would for any other function). Modules can be stored under any property name:

{
    "_id": "_design/example",
    "lib": {
        "mymodule": "exports.name = 'my module';"
    },
    "mymodule2": "exports.name = 'my module 1';"
}

You'll notice that modules can be stored under a hierachy of properties. The above design document would allow you to require 'lib/mymodule' or 'mymodule2'.

CouchDB's require

Paths are from the root of the design doc, unless explicitly relative. Relative paths start with '..' or '.' and are relative to the current module. So, from 'lib/mymodule' in the previous example, you could do require('../mymodule1').

Let's make the example design doc a little more confusing!

{
    "_id": "_design/example",
    "lib": {
        "mymodule": "exports.name = 'my module 1';"
        "mymodule2": "exports.name = 'new module';"
    },
    "mymodule2": "exports.name = 'my module 2';"
}

Now, if from 'lib/mymodule' we were to do require('mymodule2').name, what value would be returned? In this case it would be 'my module 2', not 'new module', since the path is not explicitly relative.

You cannot require modules from another design document. This is as you would expect from CouchDB.

View functions

You can't require modules from within view (map/reduce) functions. This is because CouchDB needs to know when the behaviour of a view changes, so it can rebuild the list of results.

However, in the upcoming CouchDB 1.1.x views will be able to require modules provided they exist below the 'views' property (eg, 'views/lib/module'):

{
    "_id": "_design/example",
    "lib": {
        // modules here would not be accessible from view functions
    },
    "views": {
        "lib" {
            // this module is accessible from view functions
            "module": "exports.test = 'asdf';"
        },
        "commonjs": {
            "map": function (doc) {
                var val = require('views/lib/module').test;
                emit(doc._id, val);
            }
        }
    }
}

Module caching

Currently, CouchDB does not support caching of eval'd modules. That means requiring a complex module evaluates it every time. This is obviously quite inefficient, and there is an ticket in JIRA to fix this.

One additional problem with not having a module cache is that modules cannot store state between requires. This technique can sometimes be used to cache function results, or to store a registry of templates or settings. Be careful when using libraries that depend on this.

Exactly which spec does CouchDB implement?

CouchDB initially implemented Modules 1.0 in the 0.11.0 release. This was later upgraded to Modules 1.1.1 in version 0.11.1.

Modules 1.1.1 was developed by Mikeal and the CommonJS team to overcome some problems that made Modules 1.1 difficult to implement in CouchDB.

module.exports

Using module.exports to change the exported object entirely can be quite useful, but was not added until after the Modules 1.0 spec. That means any code which requires this will not run on CouchDB 0.11.0 or earlier!

For example, using module.exports, you could have a commonjs module export a string instead of an object:

{
    "module1": "module.exports = 'test';",
    "module2": "val val = require('module1');" // val is now equal to 'test'
}

Going further

If you've found this interesting or useful, you might want to follow the progress of my experiments on pushing the module system further at kan.so.

comments powered by Disqus