Categories
Node.js Best Practices

Node.js Best Practices — Caching and REST

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.

Enabling Caching with Redis

We can enable caching with Redis to speed up our Express app.

To do this, we can install Redis by running:

apt update
apt install redis-server

Then in /etc/redis/redis.conf we change:

supervised no

to:

supervised systemd

Then Redis will run under Systemd.

Then we restart Redis to make the change take effect:

systemctl restart redis
systemctl status redis

Then we install the redis NPM module by running:

npm i redis

Then we can use it by writing:

const express = require('express')
const app = express()
const redis = require('redis')
​
const redisClient = redis.createClient(6379)
​
async function getData(req, res, next) {
  try {
    //...
    redisClient.setex(id, 3600, JSON.stringify(data))
    res.status(200).send(data)
  } catch (err) {
    console.error(err)
    res.status(500)
  }
}
​
function cache (req, res, next) {
  const { id } = req.params
​
  redisClient.get(id, (err, data) => {
    if (err) {
      return res.status(500).send(err)
    }
    if (data !== null) {
      return res.status(200).send(data)
    }
    next()
  })
}
​
​
app.get('/data/:id', cache, getData)
app.listen(3000, () => console.log(`Server running on Port ${port}`))

We use the Redis client for Node apps to create the Redis client.

Then we create the cache middleware that gets the data from the Redis cache if it exists.

We send the response data from Redis if it exists.

If it’s not, then we call our getData route middleware to get the data from the database.

This is done with the redisClient.setex method to set the data.

Enable VM/Server-Wide Monitoring and Logging

We can enable server or virtual machine monitoring and logging with various tools.

This way, we can watch for any issues that arise from the app.

The Hidden Powers of NODE_ENV

NODE_ENV can make a big difference in the performance of our Node app.

If we set it to production , caching is enabled so that data will be cached.

We don’t want that in development since we always want to see the latest data.

Views are cached in production but not in development node.

We can run it with the given NODE_ENV with:

NODE_ENV=<environment> node server.js

where server.js is the entry point of our app.

With production mode on, Express apps aren’t busy processing Pug templates all the time.

The CPU is free to do other things because of caching.

We can set the NODE_ENV by running:

export NODE_ENV=production

in Linux and Mac OS.

In Windows, we can run:

SET NODE_ENV=production

We can run:

NODE_ENV=production node my-app.js

in all platforms.

Use HTTP Methods and API Routes

We should use HTTP methods and API routes that matches REST conventions.

For example, we can write:

  • POST /article or PUT /article:/id to create a new article
  • GET /article to retrieve a list of article
  • GET /article/:id to retrieve an article
  • PATCH /article/:id to modify an existing articlerecord
  • DELETE /article/:id to remove an article

If we get by ID, we have the ID parameter at the end.

Conclusion

Caching and setting NODE_ENV to production speed up our app.

REST conventions are useful for keeping our APIs consistent.

Categories
Node.js Best Practices

Node.js Best Practices — Automation

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.

Using APM products

APM products let us discover performance issues with our apps.

It goes beyond traditional monitoring and measures the experience of users.

It can highlight the root cause of the problems in our app.

Downtimes can also be measured.

Create a Maintenance Endpoint

We can create a maintenance endpoint to show us the health of the app.

Also, we can use it for monitoring our app.

We don’t want to experiment with our app to find our data about them.

Security

There’re many obvious security-related things we should think about.

For instance, we can use VPNs to access things that shouldn’t be exposed to the public.

Also, we should secure business transactions with SSL/TLS.

SQL injection should also be avoided with stored procedures and parameterized queries.

HTTP headers and cookies that expose too much data about the internals of our system should also be avoided.

Move Frontend Assets Out of Node

Frontend assets should be moved out of our Node app since serving front end assets will bog down our app.

The single-threaded model will make this a burden.

If we serve assets from the front end, the Node thread will remain busy streaming files to users.

It won’t have much capacity to produce dynamic content.

Kill Servers Almost Every Day

Servers should be killed almost every day.

To do this, we need to use an external data store to store our app’s state.

We can’t have a local app state which we rely on.

Killing servers free up resources regularly.

Measure and Guard the Memory Usage

We should look for memory leaks in our apps.

This way, our app would free up memory for other processes.

We’ve to monitor it so that our app won’t be leaking megabytes of memory.

The max amount of memory is 1.5GB for a single Node app instance, so it’s a good idea to be efficient.

Assign Transaction ID to Each Log Statement

Logging with transaction ID lets us trace the workflow our app went through.

Because of the async nature of Node apps, this is especially important.

With th IDs, we can trace our app’s activities easier since it’s not async.

Tools that Automatically Detect Vulnerabilities

Vulnerabilities let attackers attack our app.

Security holes are fixed regularly for maintained dependencies, so we should update them often so that we can patch them.

If we see threats, we should watch for them until they’re fixed.

It’s easy to automate this with various tools like Dependabot.

Automated, Atomic and Zero-Downtime Deployments

Automated, atomic, and zero-downtime deployments are important.

This reduces risks for every deploy so we can deploy more often.

If it can be done with a click of a button, then we won’t be stressed when we’re doing it.

Atomic deployments make them easily reversible in case anything goes wrong.

We can do this easily with Docker and CI tools.

They have turned into the industry standard.

Conclusion

We can use some tools to help us with monitoring and deployment.

Also, it’s important to reduce the burden of our Node app since it’s single-threaded.

Categories
Node.js Best Practices

Node.js Best Practices — Versioning and 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.

Use Semantic Versioning

We should use semantic version to version our app.

It’s conventional so that many people will understand it.

The version should have the major version, minor version, and bug fix version and separated by dots.

So the format is major.minor.bugfix

Secure Our Applications

We should secure user and customer data so that we can protect our app against any attacks.

Things that we should be aware of include security HTTP headers, brute force attacks, and more.

We should have some headers in our HTTP response.

They include Strict-Transport-Security which enforces HTTPS connections to the server.

X-Frame-Options provides clickjacking protection.

X-XSS-Protection enables cross-site scripting filter built into most recent web browsers.

X-Content-Type-Options prevents browsers from MIME-sniff a response from the declared content type.

Content-Security-Policy prevents a wide range of attacks like cross-site scripting and other cross-site injections.

We can enable all of them with the Helmet module:

const express = require('express');
const helmet = require('helmet');

const app = express();

app.use(helmet());

There’s also the Koa version, which is the koa-helmet module.

Also, we can use it in the Nginx seer to add the headers.

In nginx.conf , we can add:

add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
add_header Content-Security-Policy "default-src 'self'";

Sensitive Data on the Client Side

We should never expose API secrets and credentials in our source code since it’ll be readable by anyone.

We can check them in code reviews.

Brute Force Protection

Brute force protection should be added to our app so that we can prevent these attacks.

To do this, we add a rate-limiting library to limit the requests that can be made.

We can use the ratelimiter package to limit the number of times a function is called:

const limit = new Limiter({ id, db });

limit.get((err, limit) => {

});

We can write Express middleware with it.

For example, we can write:

const ratelimit = require('koa-ratelimit');
const redis = require('redis');
constkoa = require('koa');
constapp = koa();

const emailBasedRatelimit = ratelimit({
  db: redis.createClient(),
  duration: 100000,
  max: 10,
  id(context) {
    return context.body.email;
  }
});

const ipBasedRatelimit = ratelimit({
  db: redis.createClient(),
  duration: 100000,
  max: 10,
  id(context) {
    return context.ip;
  }
});

app.post('/login', ipBasedRatelimit, emailBasedRatelimit, handleLogin);

We check the ID and email with our rate limit middleware with the id method.

duration sets the duration.

db is the Redi connection instance.

max is the max number of requests.

Session Management

To secure our cookies, we set a few flags.

One of them is the secure flag. This tells the browser to only send the cookie if the request is being sent over HTTPS.

HttpOnly is used to help prevent attacks like cross-site scripting since it disallows cookies to be accessed with JavaScript.

The scope of the domain should also be changed.

The domain compares against the domain of the server to which the URL is being requested.

If the domain matches, then the path has to match.

Once the path is checked then the cookie will be sent with the request.

expires is the attribute used to set persistent cookies. They expire after the expiry date has passed.

Conclusion

We should use semantic version and take some steps to secure our app.

Categories
Node.js Best Practices

Node.js Best Practices — Scaling and Technology

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 npm Scripts

We can use NPM scripts to put our scripts for builds, tests, and starting the app.

This way, we can type less to do all those tasks as we put them all in one command.

We put our scripts in package.json to make our lives easier.

So we can put:

"scripts": {  
  "preinstall": "node preinstall.js",  
  "postintall": "node postintall.js",  
  "build": "webpack",  
  "postbuild": "node index.js",  
  "postversion": "npm publish"  
}

in our file so that we can run them with npm run preinstall , npm run postinstall , etc.

If we need to run multiple commands, we can use && .

Command-line tools like Webpack, Nodemon, etc. should be run as local dev dependencies to avoid conflicts.

Use Env Vars

We should make our app configurable so that we can run them in any environment.

We can set them in various places, including the command to start our app:

NODE_ENV=production MONGO_URL=mongo://localhost:27017 nodemon index.js

or in Nodemon’s own config, which is nodemon.json:

{  
  "env": {  
    "NODE_ENV": "production",  
    "MONGO_URL": "mongo://localhost:27017/accounts"  
  }  
}

Just remember not to check in any secrets for our app.

Event Loop

If we need to perform long-running tasks, then we need to queue them in the event loop.

There’re various ways to do this.

setImmediate and setTimeout both run in the next event loop cycle.

nextTick works on the same cycle.

Use Class Inheritance

The class syntax makes inheritance easier with the extends keyword.

It makes sure that we call the parent constructor and lets us inherit things without working with prototypes directly.

Even though the class syntax is syntactic sugar for prototypes, it makes our lives easier.

Name Things Appropriately

We should name things appropriately so that we don’t have to explain the meaning of them to people.

So instead of writing:

const foo = require('morgan')  
// ...  
app.use(foo('dev'))

We write:

const logger = require('morgan')  
// ...  
app.use(logger('dev'))

Using JavaScript?

We can use extensions to JavaScript like TypeScript to make our lives easier.

They often let us restrict data types in various ways that JavaScript can’t do.

TypeScript also provides other handy features like interfaces and type aliases to restrict the structure of our objects.

It also has type guards to infer types.

The TypeScript compiler can compile to JavaScript versions as early as ES3.

It should serve anyone’s needs.

Express Middleware

Express middleware lets us make our Express app modular.

Many Express add-ons are available as middleware.

They include things like body-parser for parsing request bodies and many more.

Routes are also middlewares.

They let us build our Express app easily.

Therefore, we should know how to create and use them.

Scale Up

We rely on async code in Node so that we run code that doesn’t block other parts from running.

It only has one thread so we can’t run anything else until that piece of code is done.

To use more than one core of a processor, we’ve to create a cluster.

PM2 is a simple process manager that lets us create clusters easily.

We run:

npm i -g pm2

to install it.

Then we run:

pm2 start server.js -i 4

to run server.js with 4 instances with each instance running on its own core.

There’s also pm2-docker for Dockerized apps.

We can put:

# ...  
  
RUN npm install pm2 -g  
  
CMD ["pm2-docker", "app.js"]

in our Dockerfile to run it.

Conclusion

We can scale up with process managers and some handy tricks would help us with developing easier.

Categories
Node.js Best Practices

Node.js Best Practices — Project Structure

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.

Folder Structure

Our Node app should follow some standard folder structure.

For example, we can have something like:

src
│   app.js
└───api
└───config
└───jobs
└───loaders
└───models
└───services
└───subscribers
└───types

app.js is the app’s entry point.

api has the controllers for the endpoints.

config has the environment variables and configuration related stuff.

jobs have scheduled jobs.

loaders have the code that runs when the app starts.

models have the database models.

services has business logic.

subscribers have the event handlers for queues, etc.

types are type definitions for TypeScript projects.

3 Layer Architecture

The 3 layer architecture consists of the controllers, service layer, and the data access layer.

The controller is the interface to the client.

It takes requests and sends responses.

The service layer has the logic, which takes stuff from controllers and returns things to them.

The data access layer has the database logic which talks to the service layer.

We should never put business logic in controllers.

Separating them makes testing them easier when we write unit tests.

When we test controllers, we can just mock all the service entities.

Service Layer for Business Logic

The service layer is used for business logic.

It’s all about doing everything that controllers and database layers don’t do.

For example, we can write:

route.post('/',
  validators.userSignup,
  async (req, res, next) => {
    const userParams = req.body;
    const { user } = await UserService.Signup(userParams);
    return res.json(user);
  });

The UserService.Signup has the business logic.

The request and response are handled by the controller.

Pub/Sub Layer

The pub/sub layer is used for listening to events from external sources.

The pub part sends data to other modules.

Separating this logic makes sense since they create a cohesive layer.

Dependency Injection

Dependency injection lets us handle all the dependency initialization in one place.

For example, we can write:

export default class UserService {
  constructor(userModel, companyModel, employeeModel){
    this.userModel = userModel;
    this.companyModel = companyModel;
    this.employeeModel = employeeModel;
  }

  getUser(userId){
    const user = this.userModel.findById(userId);
    return user;
  }
}

We have the UserService class that has takes all the required dependencies in the constructor.

Then we use throughout the class.

We can use 3rd party solutions to make this easier.

The typedi library provides us with a dependency injection container to let us inject dependencies.

We can use dependency injection with Express by writing something like:

route.post('/',
  async (req, res, next) => {
    const userParams = req.body;
    const userServiceInstance = Container.get(UserService);
    const { user} = userServiceInstance.Signup(userParams);
    return res.json(user);
  });

We call the Container.get method from typedi to get the UserService so we can use it.

Cron Jobs and Recurring Task

Cron jobs and scheduled tasks should be in their own folder.

We can use the business logic from the service layer.

Also, we shouldn’t use setTimeout or another primitive way to delay the execution of code.

Instead, we should use a library to help us with this.

This way, we can control failed jobs and have feedback on ones that succeed.

Conclusion

We should create apps with a standard structure.

The folders cohesively organize different parts of our app.