Creating Better JSDoc Documentation
--
Writing code documentation is one of the most relaxing experiences of my work as a back end developer. When the company I work for, IceMobile, started to develop more and more services in Node.js instead of Java, it also became one of the most challenging bits.
I really missed Javadoc in the beginning. Back then, around 2013–14, we already had JSDoc and even though reading the documentation in the source code was alright, rendering in a way that wouldn’t drive my colleagues insane seemed an impossible task. Some annotations were not present at all in the resulting HTML file, others were hanging in the wrong places, and a remarkable amount of workarounds were needed just to get a basic, readable output. Given the amount of freedom one has when implementing code in JavaScript, the JSDoc parser seemed to have trouble identifying annotations within our most… exotic code.
One of these workarounds involved the alleged abuse of the @namespace
and @memberOf
tags all over the place, which seemed a bit wrong. But after trying to organize the documentation with other tags, it became an accepted evil.
Some months ago we had to integrate with Contentful, and I was delighted to browse their API documentation. Upon closer inspection, I realized they were also using JSDoc, and they were also heavily depending on those two tags! I was glad to see that other developers in the Node.js community had used the same approach, and I brought some of the learnings from dissecting Contentful’s source code documentation into our repertoire.
Hereby my recommendations.
Use a readable JSDoc template
No matter how well you document your source code; if you are using a barely legible template, or a template that omits certain annotations, your colleagues or users will have a hard time finding their way around the documentation.
The default JSDoc template is a bit lackluster, and the resulting HTML page is not the most readable. I would suggest using a template like docdash, which provides a clear hierarchical navigation and beautiful syntax highlighting:
The Contentful team has been kind enough to release their own JSDoc template, contentful-sdk-jsdoc, which is both beautiful and readable.
Organize your documentation using namespaces
This way we can keep our documentation organized. What the abstract concept of a “namespace” means in your project, is up to you to decide. It could be very well tied to your domain model. Given an imaginary Pet Adoption Center SDK written in Node.js, you could have namespaces like Pets, Supporters, Payments, Inventory, etc. Functions and classes belonging to each domain would be documented and annotated with the @memberOf
tag to let the JSDoc parser know that the aforementioned entity belongs to a certain namespace.
What you can also do is implement a namespace hierarchy with more than one level of depth.
Document complex entities using the @typedef
tag.
We can create reusable and complex data model documentation using the @typedef
tag. This applies to both function arguments and the data types returned by them.
If we define a type like the following…
…then we will be able to reference it everywhere else in our documentation as the type Pets.Pet
.
Make yourself familiar with all the JSDoc tags!
Seriously! Read the JSDoc documentation.
I still read the documentation of every single one of the JSDoc tags every now and then, hoping to find some functionality I missed in the previous read.
Full example, step by step
Let’s put all these learnings into a practical example. We will set up the documentation of a SDK for an imaginary Pet Adoption Center.
Setting up JSDoc in our Node.js project
Let’s start by adding the required dependencies:
> npm add --save-dev jsdoc docdash jsdoc-to-markdown
jsdoc
being the actual JSDoc module, docdash
our template and, for the sake of a more thorough example, we will also be rendering our JSDoc documentation as a Markdown file so that we can easily link it in our project’s README.md. We can achieve this with the module jsdoc-to-markdown
.
We will also centralize all the JSDoc configuration into the file jsdoc.json
:
If your SDK exports a complex hierarchy of elements, it’s handy to set the parameter useLongnameInNav
to true
. This will make sure that namespaces that reflect a “dotted hierarchy” (e.g. sdk.adoptions.dogs
) get its name displayed entirely in the resulting HTML page, and not just the last portion of the name (dogs
).
Finally, let’s add a npm script named jsdoc
in our manifest to generate the HTML documentation. The resulting package.json
file will look something like this:
This script assumes that you have a README.md file, that your source code is in the file index.js, that the resulting HTML will be placed in the folder docs/jsdoc
, and that the resulting Markdown will be in docs/API.md
.
Annotating our sample Pet Adoption Center SDK
For convenience’s sake, we will put all our source code in one single file that exports the SDK’s models and their operations. Keep in mind, this is just an example. The idea behind this SDK is that its users can operate it in the following fashion:
const sdk = require('pet-store-center-sdk');
sdk.pets.register({ name: 'Toby', type: TYPES.DOG, ... });
At this point we haven’t written any types using typedef
yet. The resulting file could look something like this:
We have declared two namespaces, one for Pets
and another for Supporters
. We have also annotated all the functions with a @memberOf
tag, specifying the namespace they to which they belong. Functions have some mandatory and some optional parameters, some of them with default values. To keep it simple, all functions will return a simple “transaction status” object with a transaction identifier and a status boolean. In the next step we will improve our documentation with types.
We will run the task npm run jsdoc
to generate the HTML, and we will have a look at it. It should be somewhat similar to this:
Let’s see what we can improve here!
Improving our documentation with types and other useful tags
If we look at the documentation generated in the previous step, one can argue that the Returns section of the Pets.adopt item doesn’t really tell us much. It’s an object, sure; but what’s in it? The pet model itself seems to be present in two functions: as the main argument of Pets.register, and also as the property pets
in the function Pets.adopt. The property paymentTier
in the function Supporters.enroll seems to be some dictionary, but what are the possible values? Let’s make things easier with types!
Our typedef-ed index.js
could look something like this:
- We have added type definitions for the different types of pets (dogs or cats) and breeds. The properties that reflect these data types have been given the appropriate type (e.g.
Pets.PetTypes
instead ofstring
). - Just for fun, we made the function
register
asynchronous, just to show how we can annotate a function that returns a promise. You can do this by defining the type asPromise<Pets.Pet>
. - Examples have been added for all the functions.
- Functions that throw an error have been annotated as such. You can go one step further and create your own error type definition.
- Also, we’ve added a type definition for the generic transaction response. This definition has been assigned to a different namespace.
- We have replaced
@param
with@property
in the pet model type definition. The@typedef
tag will not work so nicely if we use the former. - That’s a lot of annotations vs. not so much code! True. If it’s too bothersome or it becomes hard to find the right place to put type definitions, you might want to consider moving them into a separate file that only contains such annotations.
The resulting HTML page for our Pets namespace will now look much fancier!
Closing remarks and other useful tags
What about the Markdown version of the documentation?
If you remember the npm script jsdoc
we created before, we are also generating a Markdown file as part of it. If you commit this file to your GitHub repository, you can link it in the main README.md
file. However, it will not look as sexy as the HTML generated by JSDoc.
What about classes?
Components like classes or events can also be reliably part of your JSDoc documentation, and are also subject to be incorporated in your namespace hierarchy using the @memberOf
tag. Functions that might not be interpreted a functions by the JSDoc parser can be marked so by using the tag @function
.
IDE integration
The approach described in this article integrates flawlessly with IDEs like JetBrains’ WebStorm.
You will be able to navigate your way to the type definition Pet by command-clicking the word Pet. WebStorm will also recognize that the type of the argument pet
in the asynchronous function register
is of type Pets.Pet
.
Links and references
You can point your colleagues or users to parts of your documentation using the @see
tag. Given the Pet Adoption Center example, should you want to have a link to the documentation of the register
function as part of the adopt
function, you can do so with @see {Pets.register}
.
External links can be placed pretty much anywhere using the @link
tag. Check out the official documentation.
Hopefully this article will help you make your JSDoc documentation more organized and enjoyable.
Do you have tips or experiences to share about the wonderful world of documenting Node.js projects? Do hit me up!