Like any kind of apps, JavaScript apps also have to be written well.
Otherwise, we run into all kinds of issues later on.
In this article, we’ll look at some best practices we should follow when writing Node apps.
Use a Logger to Increase Error Visibility
We need to make errors visible so that we can trace and fix them.
There’re many logging tools like Pino or Winston to log errors in our app.
For example, we can write:
const logger = new winston.Logger({
level: 'info',
transports: [
new (winston.transports.Console)()
]
});
logger.log('info', 'log %s', 'foo', { anything: 'some metadata' });
logger.log
logs the error that occurs.
Test Error Flows Using a Test Framework
Testing for error cases is just as important as testing the non-error cases.
If we test the errors, then we can see if we return the right errors or not.
For example, we can write:
it('creates an item', () => {
const invalidGroupInfo = {};
return httpRequest({
method: 'POST',
uri: '/item',
resolveWithFullResponse: true,
body: invalidGroupInfo,
json: true
}).then((response) => {
expect.fail('error')
}).catch((response) => {
expect(400).to.equal(response.statusCode);
});
});
We test an API that throws an error with the some expect
function calls.
Discover Errors and Downtime Using APM products
We can check for performance issues with APM products.
They check for slow code and list the code that is slow.
Also, we can watch for downtime if there is any.
Some tools include Pingdom, Uptime Robot, and New Relic.
They let us check for issues with easy to read dashboards.
Catch Unhandled Promise Rejections
If we have an unhandled promise rejection, then we should handle them by catching them.
For example, we can write:
Promise.reject('error').catch(() => {
throw new Error('error');
});
We call catch
after the Promise.reject
to catch the rejected promise.
We can also catch ubhandledRejection
or uncaughtException
events to catch the errors.
To do that, we can write:
process.on('unhandledRejection', (reason, p) => {
throw reason;
});
process.on('uncaughtException', (error) => {
handleError(error);
if (!isTrustedError(error))
process.exit(1);
});
We can listen to those events with the process
module and then handle them our way.
reason
is the reason for the rejected promise.
The 2nd callback checks for errors and then restart for some errors.
Fail Fast and Validate Arguments Using a Dedicated Library
We should check for data with a dedicated library.
For example, we can write:
const memberSchema = Joi.object().keys({
age: Joi.number().integer().min(0).max(200),
email: Joi.string().email()
});
function addNewMember(newMember) {
Joi.assert(newMember, memberSchema);
}
The Joi
library lets us validate our object against a schema.
We validate that age
is an integer between 0 and 200.
email
lets us check for email.
Then we call Joi.assert
to do the check.
Use ESLint
ESLint lets us identify various issues including formatting, security, and bad constructs.
Therefore, we should use it to fix many of them automatically.
There’re other tools to format code consistently like prettier and beautify that have more powerful formatting options.
Node.js Specific Plugins
ESLint comes with various plugins that we can add to do more checks on Node apps.
They include things like eslint-plugin-node, eslint-plugin-mocha, and eslint-plugin-node-security.
eslint-plugin-node checks for basic Node app issues.
eslint-plugin-mocha has basic Mocha code linting.
eslint-plugin-node-security checks for security issues.
Conclusion
We can log, test and lint to improve our Node apps.