Categories
Node.js Best Practices

Node.js Best Practices — REST and Test

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 HTTP Methods & API Routes

We should follow RESTful conventions when we create our endpoints.

We should use nouns as identifiers.

For example, we have routes like:

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

Use HTTP Status Codes Correctly

Status codes should correctly tell the status of our response.

We can have the following:

  • 2xx, if everything is fine.
  • 3xx, if the resource has moved
  • 4xx, if the request can’t be fulfilled because of a client error
  • 5xx, if something went wrong on the API side.

Client-side errors are things like invalid input or unauthorized credentials.

Server-side errors are things like exceptions thrown on the server-side for whatever reasons.

We can respond with status codes in Express with res.status .

For example, we can write:

res.status(500).send({ error: 'an error occurred' })

We respond with the 500 status code with a message.

Use HTTP headers to Send Metadata

HTTP headers let us send metadata with requests and responses.

They can include information like pagination, rate-limiting, or authentication.

We can add custom headers by prefixing the keys with X- .

For instance, we can send a CSRF token with the X-Csrf-Token request header.

HTTP doesn’t define any size limit on headers.

However, Node imposes an 80KB limit as the max heart size.

The Right Framework for Our Node.js REST API

We should pick the right framework for our REST API.

There’s Koa, Express, Hapi, Resify, Nest.js, and more.

We can use the first 4 to build simple rest services.

If we need a more complete solution, we can use Nest.js.

It has things like ORM and testing built-in.

Black-Box Test Our Node.js REST APIs

To test our Node REST APIs, we can make requests to our API and check the results.

We can use a specialized HTTP client like Supertest to test our API.

For example, to test getting an article with it, we can write:

const request = require('supertest')

describe('GET /user/:id', () => {
  it('returns a user', () => {
    return request(app)
      .get('/article')
      .set('Accept', 'application/json')
      .expect(200, {
        id: '1',
        title: 'title',
        content: 'something'
      }, done)
  })
})

We make the HTTP request to the article endpoint.

And we call set to set some request headers.

Then we call expect to check the status code and response body respectively.

The data would be populated in a database that’s only used when running unit tests.

They would be reset after every test.

This ensures that we have clean data to test with.

In addition to black-box tests, we should also do unit tests for other parts like the services.

Conclusion

We should follow RESTful conventions for our APIs.

Also, testing is important for any app.

Categories
Node.js Best Practices

Node.js Best Practices — Performance and Uptime

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.

Monitoring

We must monitor our app to make sure that it’s in a healthy state.

The number of restarts, resource usage, and more indicates how our app is doing.

We use these tools to get a good idea of what’s happening in production

Delegate Anything Possible to a Reverse Proxy

Anything that can be done by a reverse proxy should be done by it.

So we can use one like Nginx to do things like SSL, gzipping, etc.

Otherwise, our app would be busy dealing with network tasks rather than dealing with the app’s core tasks.

Express has middleware for all these tasks, but it’s better to offload them to a reverse proxy because Node’s single thread model would keep our app busy just doing networking tasks.

Make Use of SSL

SSL is a given since we don’t want attackers to snoop the communication channels between our clients and the server.

We should use a reverse proxy for this so we can delegate this to it rather than using our app for SSL.

Smart Logging

We can use a logging platform to make logging and searching the logs easier.

Otherwise, everything our app is doing is a black box.

Then if troubles arise, we’ll have problems fixing issues since we don’t know what’s going on.

Lock Dependencies

Dependencies should be locked so that new version won’t be installed without changing the version explicitly.

The NPM config in each environment should have the exact version and not the latest version of every package.

We can use npm shrinkwrap for finer controls of the dependencies.

They’re locked by default by NPM so we don’t have to worry about this.

Ensure Error Management Best Practices are Met

Errors should be handled so that we have a stable app.

Error handling is different for synchronous and async code.

We got to understand their differences.

Promises call catch to catch errors.

Synchronous code uses the catch block to catch errors.

We don’t want our app to crash when it does basic operations like parsing invalid JSON, using undefined variables, etc.

Guard Process Uptime Using the Right Tool

Node processes must be guarded against failures.

This means that they’ve to be restarted automatically when they crash.

A process manager like PM2 would do this for us and more.

It also provides us with cluster management features which we get without modifying our app’s code.

There’re also other tools like Forever that does the same thing.

Use All CPU Cores

We should use all CPU cores to run our Node app.

Node.js can only run on one CPU core without clustering.

This means by default, it’ll just leave the other cores idle.

We can do that with Docker or deployment scripts based on the Linux init system to replicate the process.

Conclusion

There’re many things we can do to maximize the performance of our Node app.

We can improve logging and monitoring to make troubleshooting easier.

Categories
Node.js Best Practices

Node.js Best Practices — Nginx

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.

Adding a Reverse Proxy with Nginx

We should never expose our Express app directly to the Internet.

Instead, we should use a reverse proxy to direct traffic to our app.

This way, we can add caching, control our traffic, and more.

We can install it by running:

apt update
apt install nginx

Then we can start it by running:

systemctl start nginx

Once we have Nginx running, we can go into /etc/nginx/nginx.conf to change the Nginx config to point to our app.

For example, we can write:

server {
  listen 80;
  location / {
     proxy_pass http://localhost:3000;
  }
}

We use the proxy_pass directive to pass any traffic from port 80 to our Express app, which is listening to port 3000.

Then we restart Nginx to make our config take effect by running:

systemctl restart nginx

Load Balancing with Nginx

To add load balancing with Nginx, we can edit the nginx.conf by writing:

http {
  upstream fooapp {
    server localhost:3000;
    server domain2;
    server domain3;
    ...
  }
  ...
}

to add the instances of our app.

The upstream section creates a server group that will load balance traffic across the servers we specify.

Then in the server section, we add:

server {
   listen 80;
   location / {
     proxy_pass http://fooapp;
  }
}

to use the fooapp server group.

Then we restart Nginx with:

systemctl restart nginx

Enabling Caching with Nginx

To enable caching, we can use the proxy_cache_path directive.

In nginx.conf , we write:

http {
  proxy_cache_path /data/nginx/cache levels=1:2   keys_zone=STATIC:10m
  inactive=24h max_size=1g;
  ...
}

We cache for 24 hours with the max cache size set to 1GB.

Also, we add:

server {
   listen 80;
   location / {
     proxy_pass            http://fooapp;
     proxy_set_header      Host $host;
     proxy_buffering       on;
     proxy_cache           STATIC;
     proxy_cache_valid     200 1d;
     proxy_cache_use_stale error timeout invalid_header updating
            http_500 http_502 http_503 http_504;
  }
}

proxy_buffering set to on to add buffering.

proxy_cache is set to STATIC to enable the cache.

proxy_cache_valid sets the cache to expire in 1 day is the status code is 200.

proxy_cache_use_stale determines when a stale cached response can be used during communication.

We set it to be used when there’s a timeout error, invalid header error when the current is updated with the updating keyword.

We also can use the cache if the response status code is 500, 502, 503, or 504.

Enabling Gzip Compression with Nginx

We can enable Gzip compression with Nginx by using the gzip modules.

For instance, we can write:

server {
   gzip on;
   gzip_types      text/plain application/xml;
   gzip_proxied    no-cache no-store private expired auth;
   gzip_min_length 1000;
  ...
}

in nginx.conf to enable Gzip with gzip on .

gzip_types lets us set the MIME type for files to zip.

gzip_proxied lets us enable or disable gzipping on proxied requests.

We enable it with the private, expired, no-cache and no-store Cache-Control header values.

auth means we enable compression if a request header enables the Authorization field.

gzip_min_length means we set the minimum length of the response that will be gzipped.

The length is Content-Length response header field and it’s in bytes.

Conclusion

There’re many things we can do with Nginx to improve the performance of our apps.

Categories
Node.js Best Practices

Node.js Best Practices — Logging and Monitoring

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.

Handle Errors Centrally

We should handle errors centrally in our Node app.

For example, we can call next in one middleware to call the error handling middleware that’s added after it.

We can write:

try {
  User.addNew(req.body).then((result) => {
    res.status(200).json(result);
  }).catch((error) => {
    next(error)
  });
} catch (error) {
  next(error);
}

to catch the error in the promise chain with the catch method.

And with the try-catch block, we catch errors in the synchronous code.

Then we can add an error handler middleware after that code by writing:

app.use((err, req, res, next) => {
  errorHandler.handleError(err).then((isOperationalError) => {
    if (!isOperationalError)
      next(err);
  });
});

We invoke another promise chain to handle any operational errors.

These are errors that we can anticipate and handle gracefully.

Document API Errors Using Swagger

API errors should be documented in some way so they can be handled.

We don’t want our app to crash.

Swagger lets us make document our APIs in an automated way.

If we don’t handle errors, then the app will crash.

Shut the Process Gracefully When Unknown Errors are Encountered

If our app encounters some errors that aren’t handled, then we should shut our app down gracefully and restart it.

A common way to do this is to use a process manager like PM2 to do this.

We don’t want any error to crash our app and bring it down.

If we don’t have a process manager, then we’ll have to restart it ourselves every time.

This is definitely not acceptable since it can happen many times a day at any time.

We can create our own error handler by writing:

function errorHandler() {
  this.handleError = function(error) {
    return logger.logError(err));
  }

  this.isTrustedError = function(error) {
    return error.isOperational;
  }
}

We have an error handler constructor with some methods to handle errors.

Use a Mature Logger

If a Node app it’s in production, then the errors won’t be visible unless we log them somewhere.

There’re many logging solutions available for Node apps, including Winston, Bunyan, and Log4js.

They’ll help us find errors faster.

We shouldn’t use crude solutions like console.log for logging errors in production.

The errors should be available in some cloud services or files that we can search.

For instance, we can use Winston by writing:

const logger = new winston.Logger({
  level: 'info',
  transports: [
    new (winston.transports.Console)(),
    new (winston.transports.File)({ filename: 'somefile.log' })
  ]
});

logger.log('info', 'log: %s', 'something', { anything: 'This is metadata' });

We use the winston.Logger constructor to create the logger.

Then we call log to log what we want in the format we want.

Discover Errors and Downtime Using APM Products

Errors and downtime should be recorded with APM products.

This way, we can find them easily all in one place.

They provide features like website or API monitoring.

Each request is recorded with the performance metrics of them.

If there are slow code, it’ll record where the slowness occurred.

Also, they’ll aggregate the data and then show them together in one dashboared.

Conclusion

Errors should be handled centrally.

Also, logging and monitoring should be done with various libraries and services.

Categories
Node.js Best Practices

Node.js Best Practices — JWT and Conditional Requests

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 JWT-Based, Stateless Authentication

We can authenticate by using JSON web tokens.

A JSON web token consists of 3 parts.

They include:

  • header with the type of the token and hashing algorithm
  • payload has the claims
  • a signature that signs the payload.

We can add it easily with some add-ons.

For instance, we can use the koa-jwt package to add the token.

We can write:

const koa = require('koa')
const jwt = require('koa-jwt')

const app = koa()

app.use(jwt({
  secret: 'secret'
}))

// Protected middleware
app.use(function *(){
  this.body = {
    foo: 'bar'
  }
})

We just call app.use to use the jwt middleware.

The object has the secret to sign the token.

Then we added a protected middleware after that.

The token content will be available with this.state.user .

The JWT module doesn’t depend on any database layer.

They’re all verified on their own.

They can also contain the time to live values.

To ensure that our communication is secure, we still have to ensure that API endpoints are available through an HTTPS connection.

Use Conditional Requests

Conditional requests are HTTP requests that have different results depending on the request header values.

The headers check whether a version of a resource stored on the server match a given version of the same resource.

The headers can be the timestamp since the last modification.

It can also be an entity tag, which differs for each version.

The headers are:

  • Last-Modified which indicates when the resource was last modified
  • Etag which is the entity tag
  • If-Modified-Since which is used with the Last-Modifed header
  • If-None-Match used with the Etag header.

Use Rate Limiting

We can limit the number of requests that can be made to our API.

To tell our API clients how many requests are left, we can set the following response headers:

  • X-Rate-Limit-Limit, tells us the number of requests allows in a given time interval
  • X-Rate-Limit-Remaining, the number of requests remaining in the same interval
  • X-Rate-Limit-Reset , tells us the time when the rate limit will be reset

We can add libraries to add rate limit capabilities to our app.

With Koa, we can use the koa-ratelimit package.

Create a Proper API Documentation

It’s hard to know how to use our API without any documentation.

To make our lives easier, we can use API Blueprint or Swagger to create our documentation.

Future of APIs

There’re alternatives to REST.

We can use GraphQL to listen to HTTP requests, but it has type checking and we can selectively select resources.

With type checking and the ability to selectively query resources, we can be more efficient and reduce the chance of errors.

Conclusion

We can use JWT for authentication.

Conditional requests let us make requests differently according to headers.

Alternatives to REST APIs should also be considered.