/ Javascript

Javascript: Writing an awesome API Best Practices

When creating an API (Application Program Interface) it's crucial to have a clear idea of how you want people to interact with it and how to expose the right functions and parameters. What might seem a straightforward task at first glance, quickly can become a nightmare API that will hunt your users during midnight, and the worst part of all is that it may not be completely your fault, but first things first…

Why should I care about your opinion?

Well, the simple answer is you shouldn't, I really recommend skepticism but in a constructive way, don’t mark my words as something carved in stone but instead use them to reach to your own conclusions and opinions.

Why do I want to create an API?

Even if you are not planning on releasing your code to the public or sharing it with your teammates, it really doesn't matter, you should be writing reusable code with a clean API for yourself and most importantly for your future you, which does not want to waste time figuring out what this and that should do.

Is the effort worth it?

Well... as most things in life, it might look hard at first but once you master it and you become consistent, it transforms into a natural process that takes no extra time.

TL;DR

Don’t write bad API’s, be consistent and have always your end user and platform limitations in mind. Name things with common sense, use an object for optional parameters, and place mandatory parameters as your first arguments.

Now with things clear, let's dive into the examples and code, remember all these patterns applied to Javascript running on the browser or any runtime environment such as Node, Rhino or Nashorn.

Accessibility - Yes we have it!

Javascript lacks access modifiers such as public or private but this doesn't mean you can't make private and public like functions.

var myScope = {};
(function () {
 // Public variable
 this.myPublicVariable = "hi";
 // Private variable
 var _myPrivateVariable = "bye";
 // Public function
 this.myPublicFunction = function () {
   // Code here
 };
  // Private function
 function _myPrivateFunction () {
   // Code here
 }
}).apply(myScope);

This pattern ensures only public variables and functions can be accessed from outside “myScope”, and also provides an ES5 compatible sort of class. Note this also applies to ES6+ classes.

Although this pattern is not strictly necessary it allows you to be more specific about what your users are allowed to use, therefore during API changes and updates, fewer things need to be taken in account, that might break your user's code and experience.

Sometimes you don't want to impose restrictions over your users so they can monkeypatch everything and that's perfectly fine, but at least I recommend always pre indexing your private functions and private variables with something like an underscore. This makes clear it's up to end user own risk if they use a function that might be completely rewritten in the future.

Naming - Don't be lazy, but don't overdo it!

As obvious this might sound, I always get surprised when looking at some API’s.
Variables and functions should clearly state their purpose by just reading their name. Think for a second, if you need a descriptive comment on top of your function to understand what's going on, or constantly have to go through the documentation, it's likely that you are naming things wrong.

Don't be to light and lazy with your names, words are free, use them!
Even when you aim for a small weight lib, leave that to minification.
Also, a good editor or IDE will autocomplete this for you so really don't be lazy, otherwise you’ll be wasting more time figuring out what things do.

rectangle.move(); // Not bad, but we can do better, does it move randomly, does it dance?

rectangle.moveToPosition(); // Much better, I bet params are coords : )

In the other hand try to avoid adding things that really does not add meaningful stuff to the name or that are simply obvious and redundant.

rectangle.drawRectangle(); // Shocker, a rectangle!!! I was totally expecting it to draw a unicorn.

rectangle.draw(); // It kind of make sense, if we are talking about a rectangle API, a rectangle should be expected.

Optional parameters - Avoid endless null arguments

All functions have parameters that might simply have a default behavior or aren't needed in some cases. Many languages use functional overload to deal with this but in my opinion is a rather verbose approach and Javascript provides handy and much better ways to deal with it.

The problem, let's imagine we have a download function:

(Bad API example)

function download(dest, url, progressMonitor) {
 // Code here
}

Let’s say by default “dest” is the place where a file should be stored and if no argument is provided, it will be written to the current working directory.
This makes “dest” not mandatory whereas “url” parameter, it's kind of a must to download something since without it there would be no download in the first place.

And here is where Javascript comes to rescue with optional parameters. Simply in Javascript, not all arguments are needed to call a function, therefore:

Rule 1. ALWAYS PLACE MANDATORY PARAMETERS FIRST!!!!

Parameter url should be first since its mandatory

(Better but not perfect API example)

function download(url, dest, progressMonitor) {
 // Code here
}

Now let's say all other parameters are optional, but sometimes you want to use one or another, therefore you must call the function with null values in the right order and this might get very annoying on API changes.

download("https://awsomefile.com/file.zip", null, progressMonitor);

so here comes rule number 2 with the handy options object;

Rule 2. CONSIDER USING AN OBJECT FOR OPTIONAL ARGUMENTS

This makes your API nearly immutable to changes and this becomes extremely important when dealing with asynchronous behavior in Promises and other async flow patterns such as async await etc, but will talk about this on the next sections.

Let’s say all other parameters are optional and “options” is an object with keys and values that represent the optional parameters.

(Much better API but still not quite perfect)

function download(url, options) {
 // Code here
}

so to use this one will do:

download("https://awsomefile.com/file.zip", {
 dest: "./file.zip"
});

Now no matter how many parameters you have, your function will stay the same over time and updates.

Callback, Asynchronous and Promises - You might get surprised on this one!

There's one last obstacle, if we use callbacks, we might soon find ourselves in callback hell nesting functions and making code really hard to follow, but callbacks are needed, so where should I place them?

It turns out that when you do something asynchronously, you will ideally place the callback at the start, just like other native functions, for example, setTimeout do. Why might you be wondering? well, remember optional parameters rule number 1, if you are doing something asynchronous it's likely a callback is expected and placing it at the start its a good way for Promise like frameworks to find it fast, which is a must to get out of callback hell.

But but but…. in Node they place…. sushhhhh, don't panic and let me explain.

Imagine you have a few optional parameters, placing the callback as the first parameter of a function will effectively allow Promise frameworks to find it, and will let you opt out all consecutive arguments.

Node and many Promise frameworks, for example, enforce callbacks at the end and that's a mistake in my opinion, since it implies users writing tons of null arguments to reach to the callback, and worst of all, code written in this way will break if new parameters are added before the callback on the API.

If you have made it to here, you should remember optional parameters rule number 2 and should be saying to yourself, "hey doesn't an options parameter help here?" The shorter answer is … yes of course it helps, but not always!

a good approach will be

(Good API example, use this!)

download(url, options, doneCallback);

here no matter how many optional parameters you add, since the callback will be the third in place. Please bear in mind, that if no options are provided still a null argument must be provided unless you place callback first and if you don't, additional mandatory parameters will still break your code in future API changes.

This is a perfectly valid way of working through and as long as you follow the rules it should be ok, in fact, you will be doing better than most.

(Theoretical best API example)

download(doneCallback, url, options);

But bear in mind once again, this is incompatible with Promises and Node standard callbacks, so it's up to you.

Conclusion - The hacker way!

So let's say we agree, and callbacks should ideally be placed first, but you want to use your favorite Promise or async control framework and they tell you they should be last!

Let's prototype ourselves out of callback hell to heaven, or as I like to call it … I promise I callback! (the puns!)

Function.prototype.toNodeCb = function() {
  arguments = Array.prototype.slice.call(arguments);
  var expectedLength = this.length - 1;
  var actualLength = arguments.length - 1;
  var cb = arguments.shift();
  arguments.length = expectedLength;
  arguments.fill(null, actualLength, expectedLength);
  arguments.push(cb);
  return this;
};

Final thoughts:

This could be written the other way around to transform any Node-style callback to a theoretical best callback. Also note that depending on your application, changing back and forward from Node style to theoretical best could downgrade performance, but you could always implement a cache like a solution, it's up to you and your project needs to implement this in the best way possible so like always... stay smart. (>^_^)>