Node.js and Express

September 26th, 2020 By Alex Hu

main

After my advantage of Deno and Abc, I decided to give Node.js and Express a try, and give them fair comparison. Next.js has been a big help in getting this site up and running. This time, I want to use setup a simple private server that I can run at home to support a peronal assistant.

Node.js and Express

Instead of setting up the Node.js / Express / TypeScript from scratch, I decided to start with this barebone minimalistic starter-kit for TypeScript-based ExpressJS app . There are quite a few templates that generate more codes, but I like this barebone one as I am in a learning mode and probably worth going through very basic, especially I started with Next.js which is very well structured.

Testing NodeJs/Express API with Jest and Supertest

I want to try out the Jest testing framework and convert the test suites that I wrote in Deno.test() to test this Express server. Once all the tests pass, then the job is done.

Typescript introduced a little bit of complexity in setting up the tests, but all is good following: Testing Typescript Api With Jest and Supertest and the hello test is as simple as

import request from 'supertest'
import server from '../server'

describe('request Hello', () => {
  it('Hello API Request', async () => {
    const result = await request(server).get('/api/hello')
    expect(result.status).toEqual(200)
    expect(result.text).toContain('Hello')
  })
})

Then there is an warning message after running the test:

A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown.
Try running with --runInBand --detectOpenHandles to find leaks.

indicating that the server is still listening to the port. To tear down the server gracefully, we have to close the server connection after all the tests.

afterAll(async () => await server.close())

Continuous Integration and Continuous Delivery

I am using BitBucket to host my Git repository. The have a feature call Pipeline to enable CI/CD Continuous Integration and Continuous Delivery. So every commit will run the build, just one way to keep myself honest.

async / await

In order to use the async / await for the fs module, you can use the fs.promises module instead. For example, you can call fs.promises.stat() function like this if you want to have a convenient exists() function.

import cp from 'fs'

const fsp = fs.promises

export const exists = async (path: string): Promise<boolean> => {
  try {
    await fsp.stat(path)
    return true
  }
  catch (ex) {
  }
  return false
}

For the child_process module, you can wrap the function with the util.promisify() utility function.

import cp from 'child_process'
import util from 'util'

const execFile = util.promisify(cp.execFile)

const moveAllFiles = async () => {
  try {
    await execFile(MOVE_ALL_COMMAND)
  }
  catch (ex) {
  }
}

A couple of things that you need to watch out for:

  1. only the happy path will continue after the await, the error path will be thrown as an exception. So the exists() wrapper above is catching file not found as an exception.

  2. looping a bit more tricky. If you are use to forEach, then you are in for a big surprise. This asyncForEach seems to be a good wrapper around the loop if you want to keep your code as close as before.

async function asyncForEach(array, callback) {
  for (let index = 0; index < array.length; index++) {
    await callback(array[index], index, array);
  }
}

You can use this wrapper function as follows:

asyncForEach([1, 2, 3], async (num) => {
  await waitFor(50)
  console.log(num)
})
  1. async map also need some TLC as well. You just need to be aware of it. This is where having strong end-to-end tests will help a lot.