Node.js REST API Good Practices

Álvaro José Agámez Licha

Senior Node.js Backend Developer at S9

Agenda

  1. Motivation
  2. Metrics
  3. The initial bases
  4. Good practices
  5. Tools
  6. Code time
  7. Questions and answers

The agenda for this talk is short, I will focus on giving you a good overview of my very opinionated set of good practices alongside a healthy amount of memes.

All the talk will be based on Express, but why?, well why not?.

 

But [OTHER_LIBRARY_NAME_HERE] is faster, it's better, I like it more.

 

Yes, maybe, but we will talk about Express.

 

Express has something like 11.114.022 weekly downloads, which gives me some type of peace of mind. In any case, I think almost everything in this talk could apply to [OTHER_LIBRARY_NAME_HERE].

1. Motivation

I think that every developer more than 1 time in their career land on a project that is a complete mess and to work on these projects is hard, frustrating, time-consuming, and at least for me, very stressful.

 

So when I have the opportunity to start a new project I always try to implement the best practices in the project, and this is something that not only makes life easier for me, but also for all present and future project members.

If there's one thing I hate more than tasteless food, it's messy code, and believes me, if only I'd gotten a USD 100 bill every time I faced a spaghetti code project, without proper documentation, with a very hard to replicate development environment, without unit testing or any kind of good practices in my 17 years in the software development industry, I would be a millionaire.

2. Metrics

I like to use 2 metrics to evaluate projects I work on, WPM and FY, but actually, what do these metrics mean?

 

WPM: What the fuck Per Minute; yes, maybe could sound funny, but if you say WTF very often while looking at a project, that's a very bad sign.

 

FY: Fuck Yeah; another funny one, but this metric is the opposite of the previous one, when you say Fuck Yeah very often while looking at a project, that project is undoubtedly well built.

2. Metrics

No docker, docker-compose: MASSIVE WTF

No Unit Test: WTF, CAN I CHANGE TO ANOTHER PROJECT?

Unit Test uses a Database: WHAT THE ACTUAL FUCK

Unit Tests are not deterministic: ARE YOU SERIOUS?, WHAT THE ACTUAL FUCK, HELL NO!

No CI/CD: WTF, WTF, WTF

No Documentation: WTF, PLEASE NO

2. Metrics

We have a Rest Client Collection to test the API: Fuck Yeah

The Collection has Unit Tests: Massive Fuck Yeah

The Collection has automatic fields capture: Ohh Yeah, Fuck Yeah

I don't need to install anything except NVM on my PC: Fuck Yeah, now I'm more than happy

I can debug the API inside the docker image: Fuck Yeah, the code gods bless you.

3. The Initial Bases

I like to use some basic tools and configurations in every project to setup a minimal common base to start building my code.

 

For example, I use are EditorConfig, StandardJS, Husky, Commitizen, Conventional Commits and nvm.

EditorConfig: Helps maintain consistent coding styles for multiple developers working across various editors and IDEs.

 

StandardJS: JavaScript Standard Style.


Husky: Modern native git hooks made easy. Husky improves your commits and more 🐶 woof!.

 

Commitizen: A tool designed for teams. Its main purpose is to define a standard way of committing rules and communicating them.

 

Conventional Commits: A specification for adding human and machine readable meaning to commit messages.

 

nvm:  Is a version manager for node.js, designed to be installed per-user, and invoked per-shell.

4. Good Practices

Table of Contents

  1. Project Structure Practices
  2. Error Handling Practices
  3. Code Style Practices
  4. Testing And Overall Quality Practices
  5. Going To Production Practices
  6. Security Practices
  7. Performance Practices
  8. Docker Practices

4. Good Practices

To build a good Node.js REST API all you need:

 

  1. Common sense
  2. Design Patterns
  3. Separation of concern
  4. An industrial amount of laziness

 

That's it, with these 4 things you can build a REST API with Node.js that doesn't look like hell on earth.

1. Project Structure Practices

Structure your solution by components
The worst large applications pitfall is maintaining a huge code base with hundreds of dependencies - such a monolith slows down developers as they try to incorporate new features. Instead, partition your code into components, each gets its folder or a dedicated codebase, and ensure that each unit is kept small and simple.
 

Otherwise: When developers who code new features struggle to realize the impact of their change and fear breaking other dependent components - deployments become slower and riskier. It's also considered harder to scale-out when all the business units are not separated.

Good: Structure your solution by self-contained components.

Bad: Group your files by technical role.

1. Project Structure Practices

Layer your components (keep the web layer within its boundaries)

Each component should contain 'layers' - a dedicated object for the web, logic, and data access code. This not only draws a clear separation of concerns but also significantly eases mocking and testing the system. API developers tend to mix layers by passing the web layer objects (e.g. Express req, res) to business logic and data layers - this makes your application dependent on and accessible only by specific web frameworks.
 

Otherwise: An app that mixes web objects with other layers cannot be accessed by testing code, CRON jobs, triggers from message queues, etc.

Good: Separate component code into layers: web, services, and Data Access Layer (DAL).

Bad: The downside of mixing layers.

1. Project Structure Practices

Wrap common utilities as npm packages

In a large app that constitutes a large codebase, cross-cutting-concern utilities like a logger, encryption and alike, should be wrapped by your code and exposed as private npm packages. This allows sharing them among multiple codebases and projects.

 

Otherwise: You'll have to invent your deployment and the dependency wheel.

 

To know how to build npm packages following a set of good practices, you can read my presentation about this topic.

1. Project Structure Practices

Wrap common utilities as npm packages

1. Project Structure Practices

Separate Express 'app' and 'server'

Avoid the nasty habit of defining the entire Express app in a single huge file - separate your 'Express' definition to at least two files: the API declaration (app.js) and the networking concerns (WWW). For even better structure, locate your API declaration within components.

 

Otherwise: Your API will be accessible for testing via HTTP calls only (slower and much harder to generate coverage reports). It probably won't be a big pleasure to maintain hundreds of lines of code in a single file.

1. Project Structure Practices

Separate Express 'app' and 'server'

// app.js
const app = express()
app.use(bodyParser.json())
app.use('/api/events', events.API)
app.use('/api/forms', forms)
// bin/www/index.js
const app = require('../src/app')
const http = require('http')

// Get port from environment and store in Express.
const port = normalizePort(process.env.PORT || '3000')
app.set('port', port)

// Create HTTP server.
const server = http.createServer(app)

1. Project Structure Practices

Separate Express 'app' and 'server'

// test your API in-process using 
// supertest (popular testing package)
const request = require('supertest')
const app = express()

app.get('/user', (req, res) => {
  res.status(200).json({ name: 'tobi' })
})

request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '15')
  .expect(200)
  .end((err, res) => {
    if (err) throw err
  })

1. Project Structure Practices

Use environment-aware, secure and hierarchical config

A perfect and flawless configuration setup should ensure (a) keys can be read from files AND from environment variables (b) secrets are kept outside committed code (c) config is hierarchical for easier findability. There are a few packages that can help tick most of those boxes like rc, nconf, config, and convict.

 

Otherwise: Failing to satisfy any of the config requirements will simply bog down the development or DevOps team. Probably both.

1. Project Structure Practices

Use environment-aware, secure and hierarchical config

// config.js
const env = process.env.NODE_ENV // 'dev' or 'test'

const dev = {
 app: {
   port: parseInt(process.env.DEV_APP_PORT) || 3000
 },
 db: {
   host: process.env.DEV_DB_HOST || 'localhost',
   port: parseInt(process.env.DEV_DB_PORT) || 27017,
   name: process.env.DEV_DB_NAME || 'db'
 }
}
const test = {
 app: {
   port: parseInt(process.env.TEST_APP_PORT) || 3000
 },
 db: {
   host: process.env.TEST_DB_HOST || 'localhost',
   port: parseInt(process.env.TEST_DB_PORT) || 27017,
   name: process.env.TEST_DB_NAME || 'test'
 }
}

const config = {
 dev,
 test
}

module.exports = config[env]

2. Error Handling Practices

Use Async-Await or promises for async error handling

Handling async errors in callback style is probably the fastest way to hell (a.k.a the pyramid of doom). The best gift you can give to your code is using a reputable promise library or async-await instead which enables a much more compact and familiar code syntax like try-catch.

 

Otherwise: Node.js callback style, function(err, response), is a promising way to un-maintainable code due to the mix of error handling with casual code, excessive nesting, and awkward coding patterns.

2. Error Handling Practices

Use Async-Await or promises for async error handling

// using promises to catch errors
return functionA()
  .then(functionB)
  .then(functionC)
  .then(functionD)
  .catch((err) => logger.error(err))
  .then(alwaysExecuteThisFunction)

2. Error Handling Practices

Use Async-Await or promises for async error handling

// using async/await to catch errors
async function executeAsyncTask () {
  try {
    const valueA = await functionA();
    const valueB = await functionB(valueA);
    const valueC = await functionC(valueB);
    return await functionD(valueC);
  }
  catch (err) {
    logger.error(err);
  } finally {
    await alwaysExecuteThisFunction();
  }
}

2. Error Handling Practices

Use only the built-in Error object

Many throw errors as a string or as some custom type – this complicates the error handling logic and the interoperability between modules. Whether you reject a promise, throw an exception or emit an error – using only the built-in Error object (or an object that extends the built-in Error object) will increase uniformity and prevent loss of information.

 

Otherwise: When invoking some component, being uncertain which type of errors come in return – makes proper error handling much harder. Even worse, using custom types to describe errors might lead to the loss of critical error information like the stack trace!.

2. Error Handling Practices

Use only the built-in Error object

// doing it right
// throwing an Error from typical function, whether sync or async
if (!productToAdd) {
  throw new Error('How can I add new product when no value provided?')
}

// 'throwing' an Error from EventEmitter
const myEmitter = new MyEmitter()
myEmitter.emit('error', new Error('whoops!'))

// 'throwing' an Error from a Promise
const addProduct = async (productToAdd) => {
  try {
    const existingProduct = await DAL.getProduct(productToAdd.id)
    if (existingProduct !== null) {
      throw new Error('Product already exists!')
    }
  } catch (err) {
    // ...
  }
}

2. Error Handling Practices

Use only the built-in Error object

// anti Pattern
// throwing a string lacks any stack trace information and other
// important data properties
if (!productToAdd) {
  throw ('How can I add new product when no value provided?')
}

2. Error Handling Practices

Distinguish operational vs programmer errors

Operational errors (e.g. API received an invalid input) refer to known cases where the error impact is fully understood and can be handled thoughtfully. On the other hand, programmer error (e.g. trying to read an undefined variable) refers to unknown code failures that dictate the gracefully restart of the application.

 

Otherwise: You may always restart the application when an error appears, but why let ~5000 online users down because of a minor, predicted, operational error?. The opposite is also not ideal – keeping the application up when an unknown issue (programmer error) occurred might lead to an unpredicted behavior. Differentiating the two allows acting tactfully and applying a balanced approach based on the given context.

2. Error Handling Practices

Distinguish operational vs programmer errors

// marking an error object as operational 
const myError = new Error(
  'How can I add new product when no value provided?'
)
myError.isOperational = true

// or if you're using some centralized error factory 
// (see other examples at the bullet "Use only the 
// built-in Error object")
class AppError {
  constructor (commonType, description, isOperational) {
    Error.call(this)
    Error.captureStackTrace(this)
    this.commonType = commonType
    this.description = description
    this.isOperational = isOperational
  }
}

throw new AppError(
  errorManagement.commonErrors.InvalidInput, 
  'Describe here what happened',
  true
)

2. Error Handling Practices

Handle errors centrally, not within a middleware

Error handling logic such as mail to admin and logging should be encapsulated in a dedicated and centralized object that all endpoints (e.g. Express middleware, cron jobs, unit-testing) call when an error comes in.

 

Otherwise: Not handling errors within a single place will lead to code duplication and probably to improperly handled errors. 

2. Error Handling Practices

Handle errors centrally, not within a middleware

// DAL layer, we don't handle errors here
DB.addDocument(newCustomer, (error, result) => {
  if (error) {
    throw new Error(
      'Great error explanation comes here'
    )
  }
})

// API route code, we catch both sync and async errors and forward to the middleware
try {
  customerService.addNew(req.body).then((result) => {
    res.status(200).json(result)
  }).catch((error) => {
    next(error)
  })
} catch (error) {
  next(error)
}

// Error handling middleware, we delegate the handling to the centralized error handler
app.use(async (err, req, res, next) => {
  // The error handler will send a response
  await errorHandler.handleError(err, res)
})

process.on('uncaughtException', error => {
  errorHandler.handleError(error)
})

process.on('unhandledRejection', (reason) => {
  errorHandler.handleError(reason)
})

2. Error Handling Practices

Handle errors centrally, not within a middleware

// handling errors within a dedicated object
module.exports.handler = new errorHandler();

function errorHandler() {
  this.handleError = async (error, responseStream) => {
    await logger.logError(error)
    await fireMonitoringMetric(error)
    await crashIfUntrustedErrorOrSendResponse(error, responseStream)
  }
}

2. Error Handling Practices

Handle errors centrally, not within a middleware

// Anti Pattern: handling errors within the middleware
// middleware handling the error directly, who will handle
// Cron jobs and testing errors?
app.use((error, req, res, next) => {
  logger.logError(error)

  if (error.severity == errors.high) {
    mailer.sendMail(configuration.adminMail, 'Critical error occured', error)
  }

  if (!error.isOperational) {
    next(error)
  }
})

2. Error Handling Practices

Document API errors using Swagger

Let your API callers know which errors might come in return so they can handle these thoughtfully without crashing. For RESTful APIs, this is usually done with documentation frameworks like Swagger.

 

Otherwise: An API client might decide to crash and restart only because it received back an error it couldn’t understand. Note: the caller of your API might be you (very typical in a microservice environment). 

2. Error Handling Practices

Document API errors using Swagger

2. Error Handling Practices

Exit the process gracefully when a stranger comes into town

When an unknown error occurs (a developer error, see best practice 2.3) - there is uncertainty about the application's healthiness. Common practice suggests restarting the process carefully using a process management tool like Forever or PM2.

 

Otherwise: When an unfamiliar exception occurs, some object might be in a faulty state (e.g. an event emitter which is used globally and not firing events anymore due to some internal failure) and all future requests might fail or behave crazily. 

2. Error Handling Practices

Exit the process gracefully when a stranger comes into town

// Assuming developers mark known operational errors with
// error.isOperational=true, read best practice #3
process.on('uncaughtException', (error) => {
  errorManagement.handler.handleError(error)
  if (!errorManagement.handler.isTrustedError(error)) {
    process.exit(1)
  }
})

// centralized error handler encapsulates error-handling related logic
const errorHandler = {
  handleError: (error) => {
    return logger.logError(error)
      .then(sendMailToAdminIfCritical)
      .then(saveInOpsQueueIfCritical)
      .then(determineIfOperationalError)
  },
  isTrustedError: (error) => {
    return error.isOperational
  }
}

2. Error Handling Practices

Use a mature logger to increase error visibility

A set of mature logging tools like Pino or Log4js, will speed-up error discovery and understanding. So forget about console.log.

 

Otherwise: Skimming through console.log or manually through messy text files without querying tools or a decent log viewer might keep you busy at work until late.

2. Error Handling Practices

Use a mature logger to increase error visibility

We love console.log but a reputable and persistent logger like Pino (a newer option focused on performance) is mandatory for serious projects. High-performance logging tools help identify errors and possible issues. Logging recommendations include:

  1. Log frequently using different levels (debug, info, error).
  2. When logging, provide contextual information as JSON objects.
  3. Monitor and filter logs with a log querying API (built-in to many loggers) or log viewer software.
  4. Expose and curate log statements with operational intelligence tools such as Splunk.

2. Error Handling Practices

Use a mature logger to increase error visibility

const pino = require('pino')

// your centralized logger object
const logger = pino()

// custom code somewhere using the logger
logger.info(
  { anything: 'This is metadata' },
  'Test Log Message with some parameter %s',
  'some parameter'
)

2. Error Handling Practices

Test error flows using your favorite test framework

Whether professional automated QA or plain manual developer testing – Ensure that your code not only satisfies positive scenarios but also handles and returns the right errors. Testing frameworks like Mocha & Chai can handle this easily (see code examples within the "Gist popup").

 

Otherwise: Without testing, whether automatically or manually, you can’t rely on your code to return the right errors. Without meaningful errors – there’s no error handling.

2. Error Handling Practices

Test error flows using your favorite test framework

// ensuring the right exception is thrown using Mocha & Chai
describe('Facebook chat', () => {
  it('Notifies on new chat message', () => {
    const chatService = new chatService()
    chatService.participants = getDisconnectedParticipants()
    expect(chatService.sendMessage.bind({ message: 'Hi' })).to.throw(ConnectionError)
  })
})
// ensuring API returns the right HTTP error code
it('Creates new Facebook group', () => {
  const invalidGroupInfo = {}
  return httpRequest({
    method: 'POST',
    uri: 'facebook.com/api/groups',
    resolveWithFullResponse: true,
    body: invalidGroupInfo,
    json: true
  }).then((response) => {
    expect.fail('no error was thrown in the operation above')
  }).catch((response) => {
    expect(400).to.equal(response.statusCode)
  })
})

2. Error Handling Practices

Discover errors and downtime using APM products

Monitoring and performance products (a.k.a APM) proactively gauge your codebase or API so they can automagically highlight errors, crashes, and slow parts that you were missing.

 

Otherwise: You might spend great effort on measuring API performance and downtimes, probably you’ll never be aware which are your slowest code parts under real-world scenario and how these affect the UX.

2. Error Handling Practices

Discover errors and downtime using APM products

N|Solid is a great APM, and now have a free tier.

2. Error Handling Practices

Catch unhandled promise rejections

Any exception thrown within a promise will get swallowed and discarded unless a developer didn’t forget to explicitly handle it. Even if your code is subscribed to process.uncaughtException! Overcome this by registering to the event process.unhandledRejection.

 

Otherwise: Your errors will get swallowed and leave no trace. Nothing to worry about.

2. Error Handling Practices

Catch unhandled promise rejections

DAL.getUserById(1).then((johnSnow) => {
  // this error will just vanish
  if (johnSnow.isAlive === false) {
    throw new Error('ahhhh')
  }
})
process.on('unhandledRejection', (reason, p) => {
  // I just caught an unhandled promise rejection,
  // since we already have fallback handler for unhandled errors (see below),
  // let throw and let him handle that
  throw reason
})

process.on('uncaughtException', (error) => {
  // I just received an error that was never handled, time to handle it and 
  // then decide whether a restart is needed
  errorManagement.handler.handleError(error);
  if (!errorManagement.handler.isTrustedError(error)) {
    process.exit(1)
  }
})

2. Error Handling Practices

Fail fast, validate arguments using a dedicated library

Assert API input to avoid nasty bugs that are much harder to track later. The validation code is usually tedious unless you are using a very cool helper library like ajv and Joi.

 

Otherwise: Consider this – your function expects a numeric argument “Discount” which the caller forgets to pass, later on, your code checks if Discount > 0 (amount of allowed discount is greater than zero), then it will allow the user to enjoy a discount. OMG, what a nasty bug. Can you see it?.

2. Error Handling Practices

Fail fast, validate arguments using a dedicated library

const memberSchema = Joi.object().keys({
 password: Joi.string().regex(/^[a-zA-Z0-9]{3,30}$/),
 birthyear: Joi.number().integer().min(1900).max(2013),
 email: Joi.string().email()
})

const addNewMember = (newMember) => {
 // assertions come first
 Joi.assert(newMember, memberSchema); //throws if validation fails
 // other logic here
}

Validating complex JSON input using ‘Joi’

2. Error Handling Practices

Fail fast, validate arguments using a dedicated library

// if the discount is positive let's then redirect the user to print
// his discount coupons
function redirectToPrintDiscount(httpResponse, member, discount) {
  if (discount > 0) {
    httpResponse.redirect(`/discountPrintView/${member.id}`)
  }
}

redirectToPrintDiscount(httpResponse, someMember)
// forgot to pass the parameter discount, why the heck was the user
// redirected to the discount screen?

Anti-pattern: no validation yields nasty bugs

2. Error Handling Practices

Always await promises before returning to avoid a partial stacktrace

Always do return await when returning a promise to benefit full error stacktrace. If a function returns a promise, that function must be declared as async function and explicitly await the promise before returning it.

 

Otherwise: The function that returns a promise without awaiting won't appear in the stacktrace. Such missing frames would probably complicate the understanding of the flow that leads to the error, especially if the cause of the abnormal behavior is inside of the missing function.

2. Error Handling Practices

Always await promises before returning to avoid a partial stacktrace

async function throwAsync(msg) {
  await null // need to await at least something to be truly async (see note #2)
  throw Error(msg)
}

async function returnWithAwait() {
  return await throwAsync('with all frames present')
}

// 👍 will have returnWithAwait in the stacktrace
returnWithAwait().catch(console.log)

Calling and awaiting as appropriate

// would log
Error: with all frames present
  at throwAsync ([...])
  at async returnWithAwait ([...])

2. Error Handling Practices

Always await promises before returning to avoid a partial stacktrace

async function throwAsync () {
  await null // need to await at least something to be truly async (see note #2)
  throw Error('missing syncFn in the stacktrace')
}

function syncFn () {
  return throwAsync()
}

async function asyncFn () {
  return await syncFn()
}

// 👎 syncFn would be missing in the stacktrace because it returns a promise
// while been sync
asyncFn().catch(console.log)

Anti-Pattern: Returning a promise without tagging the function as async

// would log
Error: missing syncFn in the stacktrace
  at throwAsync ([...])
  at async asyncFn ([...])

3. Code Style Practices

Use a Linter

A lot of people use ESLint, I particularly prefer StandardJS, choose one and enforce it as your life depends on that.

 

Otherwise: Developers will focus on tedious spacing and line-width concerns and time might be wasted overthinking the project's code style.

3. Code Style Practices

Start a Codeblock's Curly Braces on the Same Line

Start a Codeblock's Curly Braces on the Same Line.

 

Otherwise: Deferring this best practice might lead to unexpected results, as seen in the StackOverflow thread below:

// Do
function someFunction() {
  // code block
}

// Avoid
function someFunction()
{
  // code block
}

3. Code Style Practices

Separate your statements properly

No matter if you use semicolons or not to separate your statements, knowing the common pitfalls of improper linebreaks or automatic semicolon insertion, will help you to eliminate regular syntax errors. Use ESLint to gain awareness about separation concerns. Prettier or Standardjs can automatically resolve these issues.

 

Otherwise: JavaScript's interpreter automatically adds a semicolon at the end of a statement if there isn't one, or considers a statement as not ended where it should, which might lead to some undesired results. You can use assignments and avoid using immediately invoked function expressions to prevent the most unexpected errors.

3. Code Style Practices

Separate your statements properly

// Do
function doThing() {
    // ...
}

doThing()

// Do
const items = [1, 2, 3]
items.forEach(console.log)

// Avoid — throws exception
const m = new Map()
const a = [1,2,3]
[...m.values()].forEach(console.log)
> [...m.values()].forEach(console.log)
>  ^^^
> SyntaxError: Unexpected token ...

// Avoid — throws exception
const count = 2 // it tries to run 2(), but 2 is not a function
(function doSomething() {
  // do something amazing
}())
// put a semicolon before the immediate invoked function, after the const definition,
// save the return value of the anonymous function to a variable or avoid IIFEs altogether

3. Code Style Practices

Name your functions

Name all functions, including closures and callbacks. Avoid anonymous functions. This is especially useful when profiling a node app. Naming all functions will allow you to easily understand what you're looking at when checking a memory snapshot.

 

Otherwise: Debugging production issues using a core dump (memory snapshot) might become challenging as you notice significant memory consumption from anonymous functions.

3. Code Style Practices

Use naming conventions for variables, constants, functions, and classes

Use lowerCamelCase when naming constants, variables, and functions, UpperCamelCase (capital first letter as well) when naming classes, and UPPER_SNAKE_CASE when naming global or static variables. Use descriptive names, but try to keep them short.

 

Otherwise: JavaScript is the only language in the world that allows invoking a constructor ("Class") directly without instantiating it first. Consequently, Classes and function-constructors are differentiated by starting with UpperCamelCase.

3. Code Style Practices

Use naming conventions for variables, constants, functions and classes

// for global variables names we use the const/let keyword and UPPER_SNAKE_CASE
let MUTABLE_GLOBAL = "mutable value"
const GLOBAL_CONSTANT = "immutable value";

// examples of UPPER_SNAKE_CASE convention in nodejs/javascript ecosystem
// in javascript Math.PI module
const PI = 3.141592653589793;

const HTTP_STATUS_OK = 200;
const HTTP_STATUS_CREATED = 201;

// for class name we use UpperCamelCase
class SomeClassExample {
  // for static class properties we use UPPER_SNAKE_CASE
  static STATIC_PROPERTY = "value";
}

// for functions names we use lowerCamelCase
function doSomething() {
  // for scoped variable names we use the const/let keyword and lowerCamelCase
  const someConstExample = "immutable value";
  let someMutableExample = "mutable value";
}

3. Code Style Practices

Prefer const over let. Ditch the var

Using const means that once a variable is assigned, it cannot be reassigned. Preferring const will help you not be tempted to use the same variable for different uses, and make your code clearer. If a variable needs to be reassigned, in a for loop, for example, use let to declare it. Another important aspect of let is that a variable declared using it is only available in the block scope in which it was defined. var is function scoped, not block-scoped, and shouldn't be used in ES6.

 

Otherwise: Debugging becomes way more cumbersome when following a variable that frequently changes.

3. Code Style Practices

Require modules first, not inside functions

Require modules at the beginning of each file, before and outside of any functions. This simple best practice will not only help you easily and quickly tell the dependencies of a file right at the top but also avoids a couple of potential problems.

 

Otherwise: Requires are run synchronously by Node.js. If they are called from within a function, it may block other requests from being handled at a more critical time. Also, if a required module or any of its dependencies throws an error and crashes the server, it is best to find out about it as soon as possible, which might not be the case if that module is required from within a function.

3. Code Style Practices

Require modules by folders, as opposed to the files directly

When developing a module/library in a folder, place an index.js file that exposes the module's internals so every consumer will pass through it. This serves as an 'interface' to your module and eases future changes without breaking the contract.

 

Otherwise: Changing the internal structure of files or the signature may break the interface with clients.

3. Code Style Practices

Require modules by folders, as opposed to the files directly

// Do
module.exports.SMSProvider = require('./SMSProvider')
module.exports.SMSNumberResolver = require('./SMSNumberResolver')

// Avoid
module.exports.SMSProvider = require('./SMSProvider/SMSProvider.js')
module.exports.SMSNumberResolver = require('./SMSNumberResolver/SMSNumberResolver.js')

3. Code Style Practices

Use the === operator

Prefer the strict equality operator === over the weaker abstract equality operator ==. == will compare two variables after converting them to a common type. There is no type conversion in ===, and both variables must be of the same type to be equal.

 

Otherwise: Unequal variables might return true when compared with the == operator.

3. Code Style Practices

Use the === operator

'' == '0' // false
0 == ''   // true
0 == '0'  // true

false == 'false' // false
false == '0'     // true

false == undefined // false
false == null      // false
null == undefined  // true

" \t\r\n " == 0 // true

3. Code Style Practices

Use Async Await, avoid callbacks

Since Node 8 LTS you can use async-await. This is a new way of dealing with asynchronous code which supersedes callbacks and promises. Async-await is non-blocking, and it makes asynchronous code look synchronous. The best gift you can give to your code is using async-await which provides a much more compact and familiar code syntax like try-catch.

 

Otherwise: Handling async errors in callback style are probably the fastest way to hell - this style forces you to check errors all over, deal with awkward code nesting, and makes it difficult to reason about the code flow.

3. Code Style Practices

Use arrow function expressions (=>)

Though it's recommended to use async-await and avoid function parameters when dealing with older APIs that accept promises or callbacks - arrow functions make the code structure more compact and keep the lexical context of the root function (i.e. this).

 

Otherwise: Longer code (in ES5 functions) is more prone to bugs and cumbersome to read.

4. Testing And Overall Quality Practices

At the very least, write API (component) testing

Most projects just don't have any automated testing due to short timetables or often the 'testing project' ran out of control and was abandoned. For that reason, prioritize and start with API testing which is the easiest way to write and provides more coverage than unit testing (you may even craft API tests without code using tools like Postman). Afterward, should you have more resources and time, continue with advanced test types like unit testing, DB testing, performance testing, etc.

 

Otherwise: You may spend long days writing unit tests to find out that you got only 20% system coverage.

4. Testing And Overall Quality Practices

Include 3 parts in each test name

Make the test speak at the requirements level so it's self-explanatory also to QA engineers and developers who are not familiar with the code internals. State in the test name what is being tested (unit under test), under what circumstances, and what is the expected result.

 

Otherwise: A deployment just failed, and a test named “Add product” failed. Does this tell you what exactly is malfunctioning?

4. Testing And Overall Quality Practices

Include 3 parts in each test name

// 1. unit under test
describe('Products Service', () => {
  describe('Add new product', () => {
    //2. scenario and 3. expectation
    it('When no price is specified, then the product status is pending approval', () => {
      const newProduct = new ProductService().add(...)
      expect(newProduct.status).to.equal('pendingApproval')
    })
  })
})
//  Anti Pattern: one must read the entire test code to understand the intent
describe('Products Service', () => {
  describe('Add new product', () => {
    it('Should return the right status', () => {
      //hmm, what is this test checking? what are the scenario and expectation?
      const newProduct = new ProductService().add(...)
      expect(newProduct.status).to.equal('pendingApproval')
    })
  })
})

4. Testing And Overall Quality Practices

Include 3 parts in each test name

Doing It Right Example: The test report resembles the requirements document.

Too Many Slides

You can read the full list of good practices that I follow here.

 

Not all the good practices that I follow are on that list, we will see that in the next section.

Code Time

Node.js REST API Good Practices

By Alvaro Agamez

Node.js REST API Good Practices

  • 400