5 NodeJS Tricks to Make JavaScript Development Fascinating
June 12th, 2021
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!
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!
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'
}
}
}
}
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
}
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')
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
And that concludes the end of this post! I hope you found this to be valuable and look out for more in the future!