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 | if (typeof Function.prototype.bind !== 'function') { |
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 | // library method iterate |
Because we know the callback right away (it is the print
function), we can collapse
the anonymous function using partial application
1 | // equivalent code |
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 | function property(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 | function foo(options) { |
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 | // library code |
Accept variadic arguments
If you are writing or
function, let it take variable number of arguments.
1 | // bad |
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 | // bad |
Alias plural properties
If a method accepts an options object, allow aliases to plural names.
1 | // bad |
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 | // bad |
Return promises from async methods
Callbacks are nasty, use promises by default.
1 | // bad |
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 | // bad |
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 | // one can see the current AngularJS version right from the browser's console |
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 | var check = require('check-more-types'); |
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 | // bad |
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 | // bad |
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 | ngDescribe(params1)(params2)(params3); |
The function can take an options argument for simplicity, cause the link between
the calls to look like a butterfly })({
1 | ngDescribe({ |
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 | // bad |
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 | // good |
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.