HomeData EngineeringData EducationBest Practices for Running Express.js in Production

Best Practices for Running Express.js in Production

What is the most important feature an Express.js application can have? Maybe using sockets for real-time chats or GraphQL instead of REST APIs? Come on, tell me. What’s the most amazing, sexy, and hyped feature you have in your Express.js application?

Want to guess what mine is? Optimal performance with minimal downtime. If your users can’t use your application, what’s the point of fancy features?

In the past four years, I’ve learned that performant Express.js applications need to do four things well:

  1. Ensure minimal downtime
  2. Have predictable resource usage
  3. Scale effectively based on load
  4. Increase developer productivity by minimizing time spent on troubleshooting and debugging

In the past, I’ve talked a lot about how to improve Node.js performance and related key metrics you have to monitor. There are several bad practices in Node.js you should avoid, such as blocking the thread and creating memory leaks, but also how to boost the performance of your application with the cluster module, PM2, Nginx and Redis.

The first step is to go back to basics and build up knowledge about the tool you are using. In our case the tool is JavaScript. Lastly, I’ll cover how to add structured logging and using metrics to pinpoint performance issues in Express.js applications like memory leaks.

In a previous article, I explained how to monitor Node.js applications with five different open-source tools. They may not have full-blown features like the Sematext Express.js monitoring integration, Datadog, or New Relic, but keep in mind they’re open-source products and can hold their own just fine.

In this article, I want to cover my experience from the last four years, mainly the best practices you should stick to, but also the bad things you should throw out right away. After reading this article you’ll learn what you need to do to make sure you have a performant Express.js application with minimal downtime.

In short, you’ll learn about:

  • Creating an intuitive structure for an Express.js application
  • Hints for improving Express.js application performance
  • Using test-driven development and functional programming paradigms in JavaScript
  • Handling exceptions and errors gracefully
  • Using Sematext Logs for logging and error handling
  • Using dotenv to handle environment variables and configurations
  • Using Systemd for running Node.js scripts as a system process
  • Using the cluster module or PM2 to enable cluster-mode load balancing
  • Using Nginx as a reverse proxy and load balancer
  • Using Nginx and Redis to cache API request results
  • Using Sematext Monitoring for performance monitoring and troubleshooting

My goal for you is to use this to embrace Express.js best practices and a DevOps mindset. You want to have the best possible performance with minimal downtime and ensure high developer productivity. The goal is to solve issues quickly if they occur and trust me, they always do.

Let’s go back to basics, and talk a bit about Express.js.

How to Structure Express.js Applications

Having an intuitive file structure will play a huge role in making your life easier. You will have an easier time adding new features as well as refactoring technical debt.

The approach I stick to looks like this:

"src/
  config/
    - configuration files
  controllers/
    - routes with provider functions as callback functions
  providers/
    - business logic for controller routes
  services/
    - common business logic used in the provider functions
  models/
    - database models
  routes.js
    - load all routes
  db.js
    - load all models
  app.js
    - load all of the above
test/
  unit/
    - unit tests
  integration/
    - integration tests
server.js
  - load the app.js file and listen on a port
(cluster.js)
  - load the app.js file and create a cluster that listens on a port
test.js
  - main test file that will run all test cases under the test/ directory"

With this setup you can limit the file size to around 100 lines, making code reviews and troubleshooting much less of a nightmare. Have you ever had to review a pull request where every file has more than 500 lines of code? Guess what, it’s not fun.

There’s a little thing I like to call separation of concerns. You don’t want to create clusterfucks of logic in a single file. Separate concerns into their dedicated files. That way you can limit the context switching that happens when reading a single file. It’s also very useful when merging to master often because it’s much less prone to cause merge conflicts.

To enforce rules like this across your team you can also set up a linter to tell you when you go over a set limit of lines in a file, as well as if a single line is above 100 characters long. One of my favorite settings, by the way.

How to Improve Express.js Performance and Reliability

Express.js has a few well known best practices you should adhere to. Below are a few I think are the most important.

Set NODE_ENV=production

Here’s a quick hint to improve performance. Would you believe that only by setting the NODE_ENV environment variable to production will make your Express.js application three times faster

In the terminal you can set it with:

export NODE_ENV=production

Or, when running your server.js file you can add like this:

NODE_ENV=production node server.js

Enable Gzip Compression

Moving on, another important setting is to enable Gzip compression. First, install the compression npm package:

npm i compression

Then add this snippet below to your code:

const compression = require('compression')
const express = require('express')
const app = express()
app.use(compression())

If you’re using a reverse proxy with Nginx, you can enable it at that level instead. That’s covered in the Enabling Gzip Compression with Nginx section a bit further down.

Always Use Asynchronous Functions

The last thing you want to do is to block the thread of execution. Never use synchronous functions! Like, seriously, don’t. I mean it.

What you should do instead is use Promises or Async/Await functions. If you by any chance only have access to sync functions you can easily wrap them in an Async function that will execute it outside of the main thread.

(async () => {
  const foo = () => {
    ...some sync code
    return val
  }
​
  async const asyncWrapper = (syncFun) => {
    const val = syncFun()
    return val
  }
​
  // the value will be returned outside of the main thread of execution
  const val = await asyncWrapper(foo)
})()

If you really can’t avoid using a synchronous function then you can run them on a separate thread. To avoid blocking the main thread and bogging down your CPU you can create child processes or forks to handle CPU intensive tasks.

An example would be that you have a web server that handles incoming requests. To avoid blocking this thread, you can spawn a child process to handle a CPU intensive task. Pretty cool. I explained this in more detail here.

Make Sure To Do Logging Correctly

To unify logs across your Express.js application, instead of using console.log(), you should use a logging agent to structure and collect logs in a central location.

You can use any SaaS log management tool as the central location, like Sematext, Logz.io, Datadog, and many more. Think of it like a bucket where you keep logs so you can search and filter them later, but also get alerted about error logs and exceptions.

I’m part of the integrations team here at Sematext, building open-source agents for Node.js. I put together this tiny open-source Express.js agent to collect logs. It can also collect metrics, but about that a bit further down. The agent is based on Winston and Morgan. It tracks API request traffic with a middleware. This will give you per-route logs and data right away, which is crucial to track performance.

Note: Express.js middleware functions are functions that have access to the request object (req), the response object (res), and the next middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named next. – from Using middleware, expressjs.com

Here’s how to add the logger and the middleware:

const { stLogger, stHttpLoggerMiddleware } = require('sematext-agent-express')
​
// At the top of your routes add the stHttpLoggerMiddleware to send API logs to Sematext
const express = require('express')
const app = express()
app.use(stHttpLoggerMiddleware)
​
// Use the stLogger to send all types of logs directly to Sematext
app.get('/api', (req, res, next) => {
  stLogger.info('An info log.')
  stLogger.debug('A debug log.')
  stLogger.warn('A warning log.')
  stLogger.error('An error log.')

  res.status(200).send('Hello World.')
})

Prior to requiring this agent you need to configure Sematext tokens as environment variables. In the dotenv section below, you will read more about configuring environment variables.

Here’s a quick preview of what you can get.

Source link

Best Practices for Running Express.js in Production 2

Handle Errors and Exceptions Properly

When using Async/Await in your code, it’s a best practice to rely on try-catch statements to handle errors and exceptions, while also using the unified Express logger to send the error log to a central location so you can use it to troubleshoot the issue with a stack trace.

async function foo() {
  try {
    const baz = await bar()
    return baz
  } catch (err) {
    stLogger.error('Function \'bar\' threw an exception.', err);
  }
}

It’s also a best practice to configure a catch-all error middleware at the bottom of your routes.js file.

function errorHandler(err, req, res, next) {
  stLogger.error('Catch-All error handler.', err)
  res.status(err.status || 500).send(err.message)
}

router.use(errorHandler)
module.exports = router

This will catch any error that gets thrown in your controllers. Another last step you can do is to add listeners on the process itself.

“process.on”(‘uncaughtException’,
err) => st Logger.error(‘Uncaught exception’, err)
throw err }) process.on(‘unhandledRejection’,
(err) => { stLogger.error(‘unhandled rejection’, err) })

With these tiny snippets you’ll cover all the needed precautions for handling Express errors and log collection. You now have a solid base where you don’t have to worry about losing track of errors and logs. From here you can set up alerts in the Sematext Logs UI and get notified through Slack or E-mail, which is configured by default. Don’t let your customers tell you your application is broken, know before they do.

Watch Out For Memory Leaks

You can’t catch errors before they happen. Some issues don’t have root causes in exceptions breaking your application. They are silent and like memory leaks, they creep up on you when you least expect it. I explained how to avoid memory leaks in one of my previous tutorials. What it all boils down to is to preempt any possibility of getting memory leaks.

Noticing memory leaks is easier than you might think. If your process memory keeps growing steadily, while not periodically being reduced by garbage collection, you most likely have a memory leak. Ideally, you’d want to focus on preventing memory leaks rather than troubleshooting and debugging them. If you come across a memory leak in your application, it’s horribly difficult to track down the root cause.

This is why you need to look into metrics about process and heap memory.

Adding a metrics collector to your Express.js application, that will gather and store all key metrics in a central location where you can later slice and dice the data to get to the root cause of when a memory leak happened, and most importantly, why it happened.

By importing a monitoring agent from the Sematext Agent Express module I mentioned above, you can enable the metric collector to store and visualize all the data in the Sematext Monitoring UI.

Best Practices for Running Express.js in Production 1

Here’s the kicker, it’s only one line of code. Add this snippet in your app.js file.

const { stMonitor, stLogger, stHttpLoggerMiddleware } =
require('sematext-agent-express')
stMonitor.start() // run the .start method on the stMonitor

// At the top of your routes add the stHttpLoggerMiddleware to send API logs to Sematext
const express = require('express')
const app = express()
app.use(stHttpLoggerMiddleware)
...

With this you’ll get access to several dashboards giving you key insight into everything going on with your Express.js application. You can filter and group the data to visualize processes, memory, CPU usage and HTTP requests and responses. But, what you should do right away is configure alerts to notify you when the process memory starts growing steadily without any increase in the request rate.

Moving on from Express.js-specific hints and best practices, let’s talk a bit about JavaScript and how to use the language itself in a more optimized and solid way.

How to Set Up Your JavaScript Environment

JavaScript is neither object-oriented or functional. Rather, it’s a bit of both. I’m quite biased towards using as many functional paradigms in my code as possible. However, one surpasses all others. Using pure functions.

Pure Functions

As the name suggests, pure functions are functions that do not mutate the outer state. They take parameters, do something with them, and return a value.

Every single time you run them they will behave the same and return a value. This concept of throwing away state mutations and only relying on pure functions is something that has simplified my life to an enormous extent.

Instead of using var or let only use const, and rely on pure functions to create new objects instead of mutating existing objects. This ties into using higher-order functions in JavaScript, like .map().reduce().filter(), and many more.

How to practice writing functional code? Throw out every variable declaration except for const. Now try writing a controller.

Object Parameters

JavaScript is a weakly typed language, and it can show its ugly head when dealing with function arguments. A function call can be passed one, none, or as many parameters as you want, even though the function declaration has a fixed number of arguments defined. What’s even worse is that the order of the parameters are fixed and there is no way to enforce their names so you know what is getting passed along.

It’s absolute lunacy! All of it, freaking crazy! Why is there no way to enforce this? But, you can solve it somewhat by using objects as function parameters.

const foo = ({ param1, param2, param3 }) => { if (!(param1 && param2 && param3)) { throw Error(‘Invalid parameters in function: foo.’) } const sum = param1 + param2 + param3 return sum } foo({ param1: 5, param2: 345, param3: 98 }) foo({ param2: 45, param3: 57, param1: 81 }) // <== the same

All of these function calls will work identically. You can enforce the names of the parameters and you’re not bound by order, making it much easier to manage.

Freaking write tests, seriously!

Do you know what’s the best way to document your code, keep track of features and dependencies, increase community awareness, gain contributors, increase performance, increase developer productivity, have a nicer life, attract investors, raise a seed round, make millions selling your startup!?…. wait that got out of hand.

Yes, you guessed it, writing tests is the answer.

Let’s get back on track. Write tests based on the features you want to build. Then write the feature. You will have a clear picture of what you want to build. During this process you will automatically start thinking about all the edge cases you would usually never consider.

Trust me, TDD works.

How to get started? Use something simple like Mocha and Chai. Mocha is a testing framework, while Chai is an assertion library.

Install the npm packages with:

npm i mocha chai

Let’s test the foo function from above. In your main test.js file add this snippet of code:

const expect = chai.expect

const foo = require('./src/foo')

describe('foo', function () {
  it('should be a function', function () {
    expect(foo).to.be.a('function')
  })
  it('should take one parameter', function () {
    expect(
      foo.bind(null, { param1: 5, param2: 345, param3: 98 }))
      .to.not.throw(Error)
  })
  it('should throw error if the parameter is missing', function () {
    expect(foo.bind(null, {})).to.throw(Error)
  })
  it('should throw error if the parameter does not have 3 values', function () {
    expect(foo.bind(null, { param1: 4, param2: 1 })).to.throw(Error)
  })
  it('should return the sum of three values', function () {
    expect(foo({ param1: 1, param2: 2, param3: 3 })).to.equal(6)
  })
})

Add this to your scripts section in the package.json:

"scripts": {
 "test": "mocha"
}

Now you can run the tests by running a single command in your terminal:

npm test

The output will be:

[email protected] test /path/to/your/expressjs/project > mocha ​ foo ✓ should be a function ✓ should take one parameter ✓ should throw error if the parameter is missing ✓ should throw error if the parameter does not have 3 values ✓ should return the sum of three values 5 passing (6ms)

This article has been published from the source link without modifications to the text. Only the headline has been changed.

Most Popular