Categories
Node.js Best Practices

Node.js Best Practices — Security

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.

Limit Concurrent Requests Using a Middleware

DOS attacks are easy for attackers to do.

They just have to flood our app with requests.

To avoid this, we can use load balances, firewalls, reverse proxies, and rate limiters packages to limit requests that can be done at a time.

There’re the rate-limiter-flexible and express-rate-limit packages to help us with this.

We can limit requests with:

const http = require('http');
const redis = require('redis');
const { RateLimiterRedis } = require('rate-limiter-flexible');

const redisClient = redis.createClient({
 enable_offline_queue: false,
});

const rateLimiter = new RateLimiterRedis({
 storeClient: redisClient,
 points: 50,
 duration: 1,
 blockDuration: 5
});

http.createServer(async (req, res) => {
  try {
    const rateLimiterRes = await rateLimiter.consume(req.socket.remoteAddress);

    res.writeHead(200);
    res.end();
  } catch {
    res.writeHead(429);
    res.end('Too Many Requests');
  }
})
 .listen(3000);

with rate-limiterr-flexible .

It’ll check the number of requests and return a 429 status code if there’re too many requests from an origin.

With express-rate-limiter, we can write:

const RateLimit = require('express-rate-limit');
app.enable('trust proxy');

const apiLimiter = new RateLimit({
  windowMs: 10*60*1000,
  max: 100,
});

app.use('/profile/', apiLimiter);

We have app.enable(‘trust proxy’); to ensure a client IP is set as the value of req.ip .

And we use express-rate-limit to limit the request window to 100 per 10 minutes.

And then we apply the middleware to the profile routes.

Extract Secrets from Config Files or Use Packages to Encrypt Them

Never store secrets in config files or source code.

We can use secret management systems to store the keys.

They can also be managed and encrypted.

We should also check with pre-commit hooks so we won’t commit secrets accidentally.

For instance, we can decrypt with the cryptr library:

const Cryptr = require('cryptr');
const cryptr = new Cryptr(process.env.SECRET);

const accessToken = cryptr.decrypt('...');

Prevent Query Injection Vulnerabilities with ORM/ODM Libraries

We should prevent SQL or NoSQL injection and other malicious attacks with a database library that escapes data that we use for queries.

Also, we should validate user input so malicious code can’t be injected into it.

Most ORM or ODMs have this basic feature.

Use SSL/TLS to Encrypt the Client-Server Connection

SSL/TLS is used almost everywhere because it provides basic security in our communication channel.

This prevents man in the middle attacks.

And so attackers can’t spy on users by tapping into the communication channel.

Comparing Secret Values and Hashes Securely

Secret values and hashes shouldn’t be compared with plain text.

We can use crypto.timingSafeEqual(a, b) to compare the hashes.

It comes 2 objects and keeps comparing evening if the data don’t match.

The default equality comparison methods only return after a character mismatch, which allows timing attacks since we can check the duration of the comparison to get a clue on how much of 2 things are matched.

Conclusion

Some basic security considerations should be taken, including storing secrets, using SSL, and prevent DOS attacks.

Categories
Node.js Best Practices

Node.js Best Practices — Production Environment

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.

Get Frontend Assets Out of Node

Our node app shouldn’t server front end assets.

Instead, we can use dedicated middleware to do this.

Node performance isn’t good when dealing with many static files since it’s single-threaded.

Be Stateless

Our app should be stateless.

This means that we should store any state in our app in external data stores.

We can kill our servers periodically or use serverless platforms like AWS Lambda that enforces stateless behavior.

This way, we can kill our server without having any downtime.

Use Tools that Automatically Detect Vulnerabilities

Any package can have vulnerabilities that put our app at risk.

This can be tamed using community and commercial tools to check for them.

This way, we can patch them as fast as we can.

We don’t want to do this tedious task manually all the time.

Assign a Transaction ID to Each Log Statement

Assigning a transaction ID to each log statement makes them easy to find.

This is because Node apps have lots of async code, so it’s hard to know the order in which things are logged without a unique ID for each entry.

Without IDs, it’s pretty hard to tell what comes first and what comes later.

Set NODE_ENV=production

We can set NODE_ENV to production so that production optimizations get activated in production.

Many NPM packages optimize their code for production.

For example, if we forgot to set NODE_ENV to production in production, Express will be slower by a factor of 3.

Automated, Atomic and Zero-Downtime Deployments

Automated deployments are essential.

They let us deploy things without hassle or risk.

They also have to be atomic so that we can reverse deployments easily in case anything goes wrong.

Any manual steps carry big risks.

If we deploy manually, then we deploy less often, and that means even bigger risks.

Use an LTS release of Node.js

The LTS version of Node.js is supported for longer with bug fixes, security updates, and performance improvements.

Therefore, we should use them so that we can keep getting updates without upgrading to a new version all the time.

If we don’t keep our Node version up to date, then we risk ourselves with vulnerabilities.

Don’t Route Logs within an App

Log destinations shouldn’t be hardcoded by developers but it should be defined by the execution environment that they’re running in.

This way, our app can log to different places according to the environment it’s running in.

Install Packages with npm ci

With npm ci , the packages are installed with the versions from package-lock.json .

A clean install is done every time we run it.

This way, we won’t have to worry about package discrepancies.

Linter Security Rules

Linter security rules help us catch security with our Node apps quickly.

eslint-plugin-security is an ESLint plugin to help us catch those in a flash.

This way, we can fix security issues that are caught right away.

Conclusion

We should follow some guidelines when we go to production.

They’ll make our lives easier and reduce risks in various ways.

Categories
Node.js Best Practices

Node.js Best Practices — Log, Test, and Lint

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.

Categories
Node.js Best Practices

Node.js Best Practices — Security and Setup

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.

Configure 2FA for NPM or Yarn

We should enable 2-factor authentication for our package repositories so that attackers can’t get to them easily.

Having it on will leave zero chance that attackers can access our repos.

Modify Session Middleware Settings

Session middleware settings should be modified to not expose anything that’ll help attackers.

For example, the X-Powered-By header should be removed.

This way, attackers can’t identify what framework we’re using.

Cookies should also be sent over a secure connection.

Then attackers can’t tap in our connection to get the information they want.

Avoid DOS Attacks by Explicitly Setting when a Process Should Crash

If our processes are overloaded, then they should crash so that we can handle these situations gracefully by catching these errors.

This means that attackers can’t keep up their DOS attacks and prolong the downtime of our services.

Also, we should alert ourselves when these events occur.

Prevent Unsafe Redirects

We shouldn’t let attackers redirect users to wherever they want.

To prevent this, we make our own URLs so that the redirects always redirect to where we want instead of where attackers want.

Avoid Publishing Secrets to the NPM Registry

The NPM registry isn’t used for storing secrets.

Therefore, we should avoid publishing any secrets there.

To do that, we can add whatever we don’t want to publish to the .npmignore file.

This way, we won’t expose API keys, passwords, and other secrets to the public.

Don’t Block the Event Loop

Our code shouldn’t block the event loop.

This is because Node apps are single-threaded.

This means that we should use async code for long-running processes.

Synchronous code that takes a long time to run will keep the rest of our app from running.

Using async code, users won’t see delays.

Prefer Native JS Methods Over 3rd Party Utilities Like Lodash

If there’re native JavaScript methods available, then we should use them instead of 3rd party libraries.

The fewer dependencies are required, the faster and smaller our app would be.

For instance, instead of Lodash array methods, we should array methods in native JavaScript.

Start Every New Project with npm init

We should start every new project with npm init so that we have a package.json in our project.

Also, we can add --yes to set all the options with the default values.

For example, we can write:

$ mkdir my-app  
$ cd my-app  
$ npm init --yes

Then we create the my-app project folder with a package.json with the default options.

Use ES6+

It’s time to use the latest JavaScript features.

They’re much better than the old syntaxes.

It’s supported by the latest versions of Node.

So we should use features like arrow functions, spread and rest, and more.

Conclusion

We should stick with some good practices like using modern features, async code, and more.

Categories
Node.js Best Practices

Node.js Best Practices — Project Setup

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.

Stick with Lowercase

We should stick with lower case file names to make the files portable.

Since Node is cross-platform, we may run into issues if we have names with different cases.

To make our lives easier, we should just use lower case names everywhere.

Cluster Our App

To make our app run with all the cores available, we should cluster our app.

One instance of our app can only use one CPU core and 1.5GB of memory.

Therefore, lots of resources may be unused if we don’t cluster them.

To go beyond the limit, we should cluster our app.

To do this, we can use forky and throng to create our clusters.

Be Environmentally Aware

We should make our app configurable so that it runs in any environment.

This way, we read all the configuration into our file.

For example, we can write:

DATABASE_URL='postgres://localhost/foobar'
HTTP_TIMEOUT=10000

in our .env file.

Then we can use some library like dotenv to read the file.

Avoid Garbage

Node will wait for the memory limit to be hit before it reclaims the unused memory.

Yo change this, we can set some settings when we run our app to reclaim the memory faster.

For example, we can run it by typing in:

node --optimize_for_size --max_old_space_size=920 --gc_interval=100 server.js

The optimize_for_size option will reclaim the memory faster.

It’ll make Node reduce memory size but compromise on speed since the unused memory is reclaimed faster.

gc-interval sets the garbage collection interval to 100ms.

max_old_space_size limits the total size of the objects in the heap.

Hooks

We can add custom hooks by adding lifecycle scripts.

To do that, we add all the scripts section of package.json to add the scripts.

For example, we can write:

"scripts": {
  "build": "bower install && grunt build",
  "start": "grunt"
}

to add 2 scripts, build and start .

Then we can run npm run build and npm start to run the scripts.

To give us more control over the scripts, we can run an external script file:

"build": "scripts/build.sh"

Track Only the Important Bits

We should only track the code files we created.

The rest should be in .gitignore so that they’re ignored.

node_modules should be ignored so that we install them with npm install instead of committing lots of package into our project repository.

This will reduce the size of our project a lot and we can do any operation like checkout, branching, etc. much faster.

If we checked in the node_modules folder, we should remove the folder by running:

echo 'node_modules' >> .gitignore
git rm -r --cached node_modules
git commit -am 'ignore node_modules'

We put node_modules into .gitignore with the first line.

Then we remove node_modules from tracking with the 2nd line.

And then we make a new commit with the change in the 3rd line.

We can ignore the npm-debug.log file the same way:

echo 'npm-debug.log' >> .gitignore
git commit -am 'ignore npm-debug'

Conclusion

There are a few things we can do with our Node project to improve it.

We can make garbage collection more frequent and we should ignore automatically produced files and folders.