Skip to main content
Table of contents

The GDS Way and its content is intended for internal use by the GDS community.

Using Node.js at GDS

This document describes how we write Node.js code at GDS. It is a list of guidelines that developers should follow in order to make code more consistent across all Node.js projects, making it easy for developers to change projects or start new ones.


The guidance set out here should be followed in conjunction with the advice on the main programming languages manual page.

The advice here is specifically about Node.js. Generic guidelines on writing code are out of scope.

If you want to contribute to this document please see the Updating this manual section below.

Most guidelines listed here are recommendations, and the authors acknowledge that there will sometimes be valid exceptions.

Node versions

Only use Long Term Support (LTS) versions of Node.js.

These are even-numbered versions (for example, Node.js 10.x or 12.x). However, it is important to keep an eye on the Node.js LTS Schedule as to when versions move in and out of LTS.

Don’t use the --harmony, --experimental-modules or other in-progress feature flags

Staged or in-progress features aren’t considered stable by the Node implementors.

Specify the supported version of Node.js in your package.json.

Use the engines key.

Source formatting and linting

Lint your JavaScript code with StandardJS

See the general JavaScript style guide for linting guidance.

Project directory structure

Organise files around features, not roles

The following structure means you don’t require lots of context switching to find related functions and your require statements don’t need overly complicated paths.

├── product
|   ├── index.js
|   ├── product.js
|   ├── product.spec.js
|   └── product.njk
├── user
|   ├── index.js
|   ├── user.js
|   ├── user.spec.js
|   └── user.njk

Don’t put logic in index.js files

Use these files to require all the modules functions.

// product/index.js
const product = require('./product');

module.exports = {
  create: product.create

Store test files within the implementation

Keeping tests in the same directory makes them easier to find and more obvious when a test is missing. Keep the project’s global test config and setup scripts in a separate test directory.

├── test
|   └── setup.spec.js
├── product
|   ├── index.js
|   ├── product.js
|   ├── product.spec.js
|   └── product.njk

See also:

Language constructs


Use const and let, avoid var

Embrace immutability (more below) and don’t let hoisting confuse you.


Prefer function for top-level function declarations and => for function literals

// Use:
const foo = function (x) {

// avoid:
const foo = x => {

// Use:
map(item => Math.sqrt(item), array);

// avoid:
map(function (item) { return Math.sqrt(item) }, array);

Acceptable exceptions include single expression functions, such as

const collatz = n => (n % 2) ? (n / 2) : ((3 * n) + 1);

or curryied functions, which are more readable using the arrow notation:

const foo = x => y => z => (x * y) + z;

Be aware that because anonymous functions don’t have a name, a stack trace will be harder to read when debugging. Also remember that arrow functions keep their context’s this.


Use the class keyword to define classes

There are many different ways to define classes in JavaScript. class has been added to the standard specifically to resolve this. In short, use class for classes and function for functions.

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  area () {
    return this.width * this.height;

Asynchronous code

Use asynchronous versions of the Node.js API functions whenever possible.

For instance, avoid readFileSync but instead use readFile. Even if your code is less readable as a result and that particular piece of code doesn’t need to be asynchronous (because you can’t proceed until you’ve read that file anyway), it won’t block other server threads. However if your program doesn’t use concurrency, synchronous versions are sometimes preferable as they are more readable and less heavy on the operating system.

Avoid inline callbacks

// Prefer:
const pwdReadCallback = function (err, data) {
fs.readFile('/etc/passwd', pwdReadCallback);

// over:
fs.readFile('/etc/passwd', (err, data) => {

// Another example:

const done = function (resolve, reject) {
  return () => {
    try {
      // Do something that might fail
      resolve('all went well');
    } catch (err) {

const waitAndSee = function (resolve, reject) {
  setTimeout(done(resolve, reject), 2500);

const log = status => message => console.log(status + ': ' + message);

new Promise(waitAndSee)

This will avoid “callback hell” and encourage organising callback functions linearly, in an event-driven fashion. It also makes it easier to write unit tests for those functions.

const pwdReadCallback = function (err, data) {
const userLoggedInCallback = function (authToken) {
const dbRequestResultCallback = function (req, res) {

Separating out a callback may pose a problem when it needs its original scope. This can be handled by currying the callback:

fs.readFile(filename, callback(filename));

const callback = filename => (err, data) => {
  if (err) {
    console.log(`failed reading ${filename}.`);
    throw err;

Functional programming

Use JavaScript’s functional programming features

JavaScript has the advantage that it offers functional programming concepts natively, like functions as first-class objects, higher-order functions (like map, reduce or apply) or pattern matching.

Following functional programming principles, such as immutable data structures and pure functions, produces code that is easier to test, less prone to runtime errors and is often more performant. Write functions as expressions that return values of a single type.

Side effects in a function can have bad consequences, especially within map or reduce. this is better avoided, as it can refer to different objects depending on the context in which a function is executed and lead to unexpected side-effects.

Remain aware that JavaScript is not just a functional language and you will most probably have to mix functional and object-oriented concepts, which can be tricky to get right.

More guidance:


Make sure you handle all errors

Envisage all error scenarios, in particular in asynchronous callback functions or Promises, and have a fallback for any situation. Consider programmer errors (bugs) as well as operational errors (arising from external circumstances, like a missing file). Bugs that are caught by exceptions should be logged and the execution stopped, and the supervisor will restart the process. Use the built-in Error object instead of custom types. It makes logging easier.

Your application shouldn’t trust any input, for example from a file or an API. So the application should handle all operational errors and recover from them.

Node.js’s HTTP server

Offload Node’s server as much as possible

Don’t expose Node’s HTTP server to the public. Use a reverse proxy to serve static assets, and cache content as much as possible. Implement adequate supervising: pm2 is recommended for Node.js-specific servers.


Stick to JavaScript

Avoid anything that compiles to JavaScript (except for static type checking, see below). Examples to avoid include CoffeeScript, PureScript and many others.

If you would like to use static typing consider using JavaScript extensions like TypeScript, or Flow. This has the advantage of making it easier to prevent runtime errors, by adding type information to variables or parameters and transpiling the source to JavaScript, reporting errors when wrong types are used.

However be aware that there are disadvantages too: augmented source code will no longer be executable uncompiled. You may also need some extra type definitions if you want to use external libraries.

Generally, anything that departs from the standard JavaScript syntax in a way that a Node developer would have trouble reading your code is advised against.


Use Express for web applications, avoid lesser-known frameworks

If you find yourself looking for an MVC framework or if you need an ORM to manage records in a database, you probably shouldn’t be using Node.js in the first place.


Avoid libraries that produce esoteric code, or that have a steep learning curve

Using advanced libraries can make code very hard to read for developers not familiar with them, as useful as they may be. For instance, Ramda lets you write:

 [, R.identity],
 [, parseInt],
 [R.T, R.always(NaN)]

which is compact and useful, but not easily understood up by a developer not familiar with it.

Generally, readable code is better than compact or advanced code. Optimisation can lead to very arcane code, so should only be used when necessary.

// Prefer:
for (let i = 0; i < 10; i += 1) {
  for (let j = 0; j < 10; j += 1) {
    console.log(i, j);

// Over:
for (let i=0,j=0;i<10 && j<10;j++,i=(j==10)?i+1:i,j=(j==10)?j=0:j,console.log(i,j)){}

Node Package Manager (NPM)

Don’t reinvent the wheel, but don’t over-use the amount of external code you import.

Using other people’s code is a risk. NPM is no exception and is arguably worse than other package managers as the small granularity of packages leads to a very large number of dependencies. Your application may easily end up relying on software written by hundreds of different people, most of whom you don’t know.

However, given that it’s impossible not to depend on foreign code, you should do your best to reduce the risks involved:

  • avoid relying on packages for functionality you can easily implement yourself, especially if a package has a lot of functionality you don’t need.
  • empirically check the trustworthiness of a package you wish to use: check its popularity, author reputation, etc.
  • Use tools like GitHub security alerts or Snyk to check packages for vulnerabilities

Use npm init, npm start, npm script, etc.

Making full use of NPM’s features will simplify your continuous integration and deployment.

Lock down dependencies

Avoid incompatible upgrades that may break your application. NPM 5 does it by default, but be careful when upgrading or adding a new package. Tools like Greenkeeper can be useful to automatically check dependencies as part of your continuous integration.

Regularly check for unused dependencies, and make sure you don’t have dev dependencies in production

Don’t slow down your deployments with unused code.

If you build modules you think could be useful for others, publish them on npm

Because Open Source is a Good Thing.

Published modules should be tested and supported on the last two current LTS versions of Node.js.

See also:

Starting a new project

We recommend starting any new project with the GDS Node.JS boilerplate. It makes it easier to create new serves and follows the advice given here. Is also includes the GOV.UK styles and templates, StandardJS, grunt and snyk.

Further reading

In the GDS context, the guidelines provided in the links below are advisory only. The guidance provided here takes precedence in case of conflicts.

Updating this manual

This manual is not presumed to be infallible or beyond dispute. If you think something is missing or if you’d like to see something changed then:

  1. (optional) Start with the #Nodejs community’s Slack channel to see what other developers think. You will then understand how likely it is that your proposal will be accepted as a pull request before you complete any work.
  2. Check out the making changes section of the GDS Tech repo.
  3. Create a pull request against GDS Tech repo.
This page was last reviewed on 7 November 2019. It needs to be reviewed again on 7 May 2020 by the page owner #gds-way .
This page was set to be reviewed before 7 May 2020 by the page owner #gds-way. This might mean the content is out of date.