User friendly API

How to design a simple to use and powerful library API.

Check the environment before starting

Check the runtime environment before starting - maybe a required dependency or a platform feature is missing? Throw a descriptive error right away. For example, check-more-types uses Function.prototype.bind method that might be missing in some older browsers (most notably, in the PhantomJS < version 2.0.0)

1
2
3
if (typeof Function.prototype.bind !== 'function') {
throw new Error('Missing Function.prototype.bind, please load es5-shim first');
}

Design the arguments order

If you do not use a single options arugment, then you need to carefully think about the most user-friendly argument order. I usually recommend to place the data known early at the left-most place. For example, if we iterate over the values stored in an array, we usually know the callback function right away - it is part of the code base. We do not know the actual array, it is often generated dynamically. Thus I recommend to place the callback first

1
2
3
4
5
6
7
8
9
10
11
// library method iterate
function iterate(cb, list) {
list.forEach(cb);
}
// user code
function print(x) {
console.log(x);
}
ajax.get('/get/me/a/list', function (data) {
iterate(print, data);
});

Because we know the callback right away (it is the print function), we can collapse the anonymous function using partial application

1
2
// equivalent code
ajax.get('/get/me/a/list', iterate.bind(null, print));

Well designed argument order leaves itself well-suited for left-to-right partial application. The partial application is available in JavaScript today via .bind method.

Forgive the wrong argument order

If the arguments can be unambiguously distinguished using their types, allow the user to specify them in any order. This removes the need to consult the API documentation.

1
2
3
4
5
6
7
function property(object, propertyKey) {
// if called with property('foo', { foo: 'bar' })
if (typeof object === 'string' && typeof propertyKey === 'object') {
return property(propertyKey, object);
}
return object[propertyKey];
}

Accept a single options object

If you have more than 2 function arguments, it is a pain to remember the right order, or specify default values. In this case let your function take an options object instead

1
2
3
4
5
6
function foo(options) {
options = options || {};
if (options.bar) {
...
}
}

You can still use partial application using a helper library like obind

Curry methods where appropriate

Closely related to designing the argument order is adding currying of methods where appropriate. This removes the need to do the partial application. I prefer to add the currying right before exporting the function.

1
2
3
4
5
6
7
8
9
10
// library code
function property(propertyKey, object) {
return object[propertyKey];
}
module.exports.property = R.curry(property);
// client code
var getName = library.property('name');
console.log('Name of the person', getName(person));
// or use property with 2 arguments
console.log('Age of the person', library.property('age', person));

Accept variadic arguments

If you are writing or function, let it take variable number of arguments.

1
2
3
4
5
// bad
library.or(A, B); //=> A || B
library.or(A, B, C); //=> A || B, C is ignored!
// good
library.or(A, B, C); //=> A || B || C

At least show a warning message if the number of arguments is larger or smaller than expected, do not silently ignore the runtime arguments.

Alias plural names

Provide plural versions for each method that accepts more than one parameter of the same type

1
2
3
4
5
6
7
8
9
// bad
library.is.number(42);
library.is.number(42, 10); // hard to read
// good
library.is.number(42);
library.is.numbers(42, 10); // simple and convenient to read
// even better
library.is.number(42);
library.are.numbers(42, 10);

Alias plural properties

If a method accepts an options object, allow aliases to plural names.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// bad
library.compute({
number: 42
});
library.compute({
number: [42, 10, 20]
});
// good
library.compute({
number: 42
});
library.compute({
numbers: [42, 10, 20]
// numbers is an alias to number
});

Allow a single value in addition to Array with 1 element

If the method expects 1 or more values, usually passed as an array, allow single items to be passed without putting them into an array with 1 element.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// bad 
library.compute({
numbers: [1, 2, 3]
});
library.compute({
numbers: [1]
});
// good
library.compute({
numbers: [1, 2, 3]
});
library.compute({
numbers: 1
});
// best
library.compute({
number: 1
});

Return promises from async methods

Callbacks are nasty, use promises by default.

1
2
3
4
5
6
// bad
library.get('/foo/bar', function (err, data) { ... });
// good
library.get('/foo/bar')
.then(function (data) { ... })
.catch(function (anyError) { ... });

When in doubt, return a promise.

Throw meaningful errors when validating inputs

There is nothing worse than trying to call a method from someone's library and get a cryptic "invalid inputs" error. Please validate the inputs and either throw individual errors, or use a helper method like check.defend.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
function div(a, b) {
return a/b;
}
// good
function div(a, b) {
la(b, 'cannot divide', a, 'by zero');
}
// better
function div(a, b) {
return a/b;
}
module.exports = check.defend(div,
check.number, 'division works with numbers only',
check.not.zero, 'cannot divide by zero'
);

Make API complete

Think which methods make sense to add all at once in order to avoid missing features. For example, when writing a library of predicates, if you provide method library.or, it makes sense to also provide library.and, library.not, etc. It is very frustrating to use a library and then discover that is missing something that was very simple to add.

Attach version and meta information

Do not let your library's users guess what version they are running. Attach the version string and other meta information right to the exported object / function. I would not count on putting this information into the comments, since the comments can be stripped during the minification step.

1
2
3
// one can see the current AngularJS version right from the browser's console
angular.version
Object {full: "1.4.0", major: 1, minor: 4, dot: 0, codeName: "jaracimrman-existence"}

You can read how to attach the version information in this blog post

Provide a method for extending the library

If it makes sense, add a method for the users of your library to extend it with the new custom methods. For example, the check-more-types library exposes the method it uses internally for adding new predicates, allowing the user to create custom checks and getting all combinations with modifiers .not and .maybe right away.

1
2
3
4
5
6
7
8
9
var check = require('check-more-types');
function isValidAge(x) {
return check.number(x) &&
x > 0 && x < 140;
}
check.mixin(isValidAge, 'age');
check.age(20); // true
check.maybe.age(10); // true
check.not.age(0); // true

Provide lots of examples in your documentation

We learn best by looking through examples (see lodash.com API documentation). Please provide plenty of examples for each method. You can automate example generation using a tool like xplain that converts unit tests into examples inside your docs. This way you never have to maintain them separately or worry about the examples being out of date.

Use fluent interface

If the library operations operate on the same piece of data, consider fluent interface so that the method calls are chainable.

1
2
3
4
5
6
7
8
// bad
var person = library.getPerson();
person.setName('john');
person.setAge(21);
// good
var person = library.getPerson();
person.setName('john')
.setAge(21);

You can see how to implement fluent helper in this blog post

You can provide chaining even for purely functional style, for example see lodash#chain

1
2
3
4
5
6
7
8
// bad
var filtered = _.filter(list, predicateFn);
_.each(filtered, print);
// good
_(list)
.filter(predicateFn)
.each(print)
.value();

Fluent functional interface + butterfly

There is a way to have fluent functional interface: just return the function itself. In this case the function will have side-effects, but it is really convenient to use. For example testing library ng-describe has a single function that returns itself and allows grouping tests easily.

1
2
ngDescribe(params1)(params2)(params3);
// ngDescribe(params1) returns ngDescribe again

The function can take an options argument for simplicity, cause the link between the calls to look like a butterfly })({

1
2
3
4
5
6
7
ngDescribe({
params1
})({
params2
})({
params3
});

Bind methods to an instance

If you are returning an object with methods to be called, bind the method to the object to allow point-free use. For example, we need to always invoke api.printName to print the api's name.

1
2
3
4
5
6
7
8
9
10
11
12
13
// bad
// library
module.exports = {
name: 'foo',
printName: function () {
console.log(this.name);
}
};
// user code
var api = require('api');
promise.then(function () {
api.printName();
});

Console object in most browsers suffers from the same problem, that is why I use shortcut for printing the resolved value

1
promise.then(console.log.bind(console));

We can change the API to bind the printName before returning it from the library

1
2
3
4
5
6
7
8
9
10
11
12
13
// good
// library
var api = {
name: 'foo'
};
function printName() {
console.log(this.name);
}
api.printName = printName.bind(api);
module.exports = api;
// user code
var api = require('api');
promise.then(api.printName);

If you have complicated API, you can bind all methods in the exported object (or some of them) using a helper function _.bindAll

Conclusions

Try to make your API as user-friendly as possible by

  • Using aliases to make the API read naturally
  • Using aliases and being flexible about unambiguous input to minimize the number of times the user has to consult the API documentation.