5 NodeJS Tricks to Make JavaScript Development Fascinating

Christopher T.

June 12th, 2021

Share This Post

NodeJS is one of the most popular platforms to develop applications due to its blazing fast abilities to process I/O operations. But there are plenty of more good reasons that contribute to its success in popularity.

This post will go over some useful tricks you can use in NodeJS right now to enhance your development experience to the point you will become fascinated. Most of these tips are possible thanks to the flexible nature of the JavaScript language.

Without further ado, lets begin!

1. Inspecting your code in the console, enhanced

I think we've all been there--placing our calls to console.log all over our functions during development. Debugging code can become quite dull because most of the info that we need aren't really printed in the console by default.

For example, imagine this Page class below where it has some state about the previous page, current page, history of pages navigated to, etc after navigating:

class Page {
  #state = {
    previous: '',
    current: '',
    history: [],
  }
  authenticated = false

  get state() {
    return this.#state
  }

  async authenticate() {
    const response = await fetch('https://authenticateme.com/api', {
      method: 'post',
      body: {
        username: 'hello',
        password: 'abc123',
      },
    })
    const data = await response.json()
    this.authenticated = data.role === 'admin'
    return this.authenticated
  }

  async navigate(pathname) {
    if (typeof window !== 'undefined') {
      window.location.href = pathname
    }
    this.#state.history.unshift(pathname)
    this.#state.previous = this.currrent
    this.#state.current = pathname
  }

  snapshot() {
    return {
      ...this.#state,
      historySize: this.#state.history.length,
      authenticated: this.authenticated,
    }
  }
}

If we try to log our Page instance after navigation and want to check if our instance is behaving correctly, we are given this output:

Page { authenticated: false }

Wow, this is very useful! Not. Luckily the NodeJS team has been graceful because they've provided an easy convenience feature (along with others that you will find in this post) that we can use in our code immediately to improve our output.

All you have to do is to attach the line below as a method to an object and return an object which will be printed to the console:

Symbol.for('nodejs.util.inspect.custom')

For example, we can attach this onto our Page class like so:

class Page {
  #state = {
    previous: '',
    current: '',
    history: [],
  }
  authenticated = false;

  [Symbol.for('nodejs.util.inspect.custom')]() {
    return {
      ...this.snapshot(),
    }
  }

  get state() {
    return this.#state
  }

  async authenticate() {
    const response = await fetch('https://authenticateme.com/api', {
      method: 'post',
      body: {
        username: 'hello',
        password: 'abc123',
      },
    })
    const data = await response.json()
    this.authenticated = data.role === 'admin'
    return this.authenticated
  }

  async navigate(pathname) {
    if (typeof window !== 'undefined') {
      window.location.href = pathname
    }
    this.#state.history.unshift(pathname)
    this.#state.previous = this.currrent
    this.#state.current = pathname
  }

  snapshot() {
    return {
      ...this.#state,
      historySize: this.#state.history.length,
      authenticated: this.authenticated,
    }
  }
}

Now when we log our Page to the console:

const page = new Page()

page.navigate('/login').then(() => {
  console.log(page)
})

We can see our snapshot result as the output instead:

{
  "previous": undefined,
  "current": "/login",
  "history": ["/login"],
  "historySize": 1,
  "authenticated": false
}

Wonderful!

2: Stringification

In addition to our previous tip there is also a neat trick where we can customize the stringification of objects.

For example, just as before, the default stringication output of our Page class when called by toString() comes out to something that isn't very useful to us like this:

[object Object]

The reason why this matters is because the return value of toString() is also used when we assign them as properties since it gets called by default:

const page = new Page()

page.navigate('/login').then(() => {
  const ourObject = {
    fruits: ['apple', 'banana'],
    [page]: page.snapshot(),
  }
  console.log(ourObject)
})

Output:

{
  "fruits": ["apple", "banana"],
  "[object Object]": {
    "previous": undefined,
    "current": "/login",
    "history": ["/login"],
    "historySize": 1,
    "authenticated": false
  }
}

Luckily we can overwrite the default toString() method and provide a more proper way to serialize into a key:

toString() {
    return JSON.stringify(this.snapshot())
  }
const output = {
  fruits: ['apple', 'banana'],
  '{"current":"/login","history":["/login"],"historySize":1,"authenticated":false}':
    {
      previous: undefined,
      current: '/login',
      history: ['/login'],
      historySize: 1,
      authenticated: false,
    },
}

This can be useful if we want to cache and revive (or the more convential term "rehydrate") previous states of our app to provide a better user experience for users. For example, if that output was cached into local storage and the user exits, then comes back 7 hours later we can make our app rehydrate itself by picking up the last state that was cached, and initializing itself accordingly:

localStorage.setItem('appState', page)
window.addEventListener('load', () => {
  const cachedState = localStorage.get('appState')
  const page = new Page(JSON.parse(cachedState))
})

The Page constructor can just take that as an argument and initiate its state accordingly:

constructor(initialState) {
  for (const [key, value] of Object.entries(initialState)) {
    if (key in this.#state) {
      this.#state[key] = value
    } else if (key === 'authenticated') {
      this.authenticated = value
      if (!window.location.pathname.endsWith('/admin')) {
        window.location.href = '/admin'
      }
    }
  }
}

3. Jsonification

Similarly to the previous tips, it's also possible to customize the output when we call JSON.stringify on our Page class. By default we are given this when used:

const page = new Page()

page.navigate('/login').then(() => {
  console.log(JSON.stringify(page))
})

Output:

{ "authenticated": false }

This brought us back to square one when we directly logged Page in the beginning.

When we overrided the default toString() method so that it can get converted to the string we wanted earlier, we had to call page.snapshot in order to stringify the output we want:

page.navigate('/login').then(() => {
  const ourObject = {
    fruits: ['apple', 'banana'],
    [page]: page.snapshot(),
  }
  console.log(ourObject)
})

We can actually make the snapshot become the output of calling JSON.stringify on it (and this is better because it aligns more with the nature of JSON.stringify and JSON.parse being used to serialize data when working with APIs)

All you have to do is define a custom toJSON method like so:

snapshot() {
  return {
    ...this.#state,
    historySize: this.#state.history.length,
    authenticated: this.authenticated,
  }
}

toString() {
  return JSON.stringify(this.snapshot())
}

toJSON() {
  return this.snapshot()
}

Now when we pass Page to JSON.stringify it gives us the snapshot:

const page = new Page()

page.navigate('/login').then(() => {
  console.log(JSON.stringify(page))
})
{
  "current": "/login",
  "history": ["/login"],
  "historySize": 1,
  "authenticated": false
}

4. Clearing terminal the right way

You're probably familiar with console.clear which seemingly clears the console. But when you scroll up, the previous output is actually still there.

The correct way to clean all of that data and start fresh is this:

process.stdout.write('\x1Bc')

Since this is a common need for me it is incredibly useful that I have this set as a User Snippet on VSCode to generate it automatically when I type clear (it can be another keyword like "cls", but I chose clear)

To put this into your snippets, press Ctrl+P and select Preferences: Configure User Snippets, and choose typescript.json (or javascript.json, or both, or even typescriptreact.json to enable for .tsx files as well) and put this inside the json object:

{
  "clear": {
    "prefix": "clear",
    "body": "process.stdout.write('\\x1Bc')"
  }
}

Now when you type in clear in a .ts file and hit enter it will generate:

process.stdout.write('\x1Bc')

5. Pre, Post

We all work with the usual start and build commands while we develop are applications found in package.json:

"scripts": {
  "start": "gatsby develop --port 3000",
  "build": "gatsby build",
  "test": "jest"
}

But something that not all of us might know is that we can define another script to run before the main script is done executing and/or after its done.

For example, to run a file after users run npm install on your repo, all you have to do is add another script command with the prefix post before the script name. For example:

"scripts": {
  "postinstall": "./scripts/postinstall.js",
  "start": "gatsby develop --port 3000",
  "build": "gatsby build",
  "test": "jest"
}

Here is a real example where Gatsby uses this to automatically print a welcoming message to the user after running npm install on their repo.

These two prefixes can be used for any script and it will still run the matching script with the pre prefix prior to the main script and the one with post afterwards, so you can have something like:

"scripts": {
  "postinstall": "node ./scripts/postinstall.js",
  "start": "gatsby develop --port 3000",
  "build": "gatsby build",
  "test": "jest",
  "pregenerate": "node ./scripts/init-directories.js",
  "generate": "node ./scripts/fetch-data.js",
  "postgenerate": "node ./scripts/upload-data.js"
}

Read more tricks to npm scripts here

Conclusion

And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!


Tags


Subscribe to the Newsletter
Get continuous updates
© jsmanifest 2021