OpenJS Node.js Application Developer
05 September, 2022
Resources
Setting up
- Node works on multiple platforms including Windows, macOS, and Linux.
- Do not use unofficial or OS package managers (apt, brew, chocolatey, etc) to install Node. This can create problems related to the version on binary placement.
- Do not install global packages using
sudo
. It grants them root privileges that shouldn’t be required. - Instead of installer available on the Node website, use a version manager.
- Uninstall Node before installing it using a version manager.
- The recommended way to install Node is using nvm (Node Version Manager).
- nvm works on all the major shells besides Fish.
- Use the shell script found on the Github repo to install nvm.
- To install a specific version of node using nvm run
nvm install 12
. - Verify Node is installed using
node -v
- nvm isn’t available on Windows and instead, you can use the alternative
nvs
. - You can set the Node version using
nvm add 12
andnvm use 12
.
Lab check
- Check the node version. Use
node -v
. A number prefixed withv
should be printed on the screen (eg v12.16.3). - Check the npm version. Use
npm -v
. The installed npm version should be printed (6.14.4).
Knowledge Check
- Recommended approach to install Node: a version manager.
- Check the version on installed Node: node -v.
- Besides Node what else is provided: a module package manager.
Node Binary
- All Node programs are represented and executed by the
node
binary.node app.js
. - To see all the available flags run:
node --help
. - To view V8 runtime specific flags run:
--v8-options
. - To parse and check the application without running it:
node --check app.js
ornode -c app.js
. - To evaluate and print code:
node --print "1+1"
ornode -p "1+1"
- To just evaluate code without any output run:
node --eval "1+1"
ornode -e "1+1"
- When printing or evaluating, Node core modules don’t need to be required. They are a part of the namespace.
node -p "fs.readdirSync('.').filter(f => /.js$/.test(f))"
. - To preload, a module:
node --require preload.js app.js
ornode -r preload.js app.js
. - The default stack trace has 10 stack frames.
- Stack trace limit can be increased using
--stack-trace-limit
flag which is a V8 option. - To increase the stack trace limit:
node --stack-trace-limit=101 app.js
. - For security reasons keep the default stack trace limit on production.
- Flags should appear before the filename.
Lab check
- Set stack trace limit to 200:
--stack-trace-limit=200
- Check the syntax:
--check app.js
Knowledge Check
- Preload: -r or –require
- Check: -c or –check
Debugging and Diagnostics
- To debug an application the Node process should be started in the inspect mode.
- We use Chrome Devtools to debug the node process.
- To start the program in inspect mode:
node --inspect app.js
. - However, if the program isn’t long-running or the breakpoint hasn’t been set, it wouldn’t pause.
- To pause the program immediately:
node --inspect-brk app.js
. - The remote debugging protocol uses Websockets.
- To access the debugger go to
chrome://inspect
. - Use Pause on caught exceptions when there is an error. This can help find locations that aren’t obvious.
- The Call stack can be viewed in Developer Tools.
- You can add a breakpoint in the Developer Tool by clicking on the line number.
- Once the code execution is paused on the breakpoint, you can over on the variable to check their value.
- To set a breakpoint using code add the
debugger
keyword. It should be removed after use.
Lab check
node --inspect-brk app.js
and enable Pause on caught exceptions.- Add
debugger
on the first line of functionf
.
Knowledge Check
debugger
to set a breakpoint using code.--inspect-brk
to pause of the first line.
Key JavaScript Concepts
Data Types
- 7 primitive data types: null, undefined, number, boolean, string, symbol, BigInt.
- Everything else is an object: function, arrays, etc.
- Any variable defined without value or function without return would be undefined.
- The number is a double-precision floating-point. Allowing integer range of -253-1 to 253-1.
- BigInt has no limits.
- Strings can be created with single or double quotes or backticks.
- Backtick strings can be multiline and support interpolation.
- Symbols can be used for unique object keys.
.for
method is for creating/getting global symbols. - An object is a set of key-value pairs. Key is called property and the value is either a primitive or an object.
- All JavaScript objects have prototypes.
Functions
- Functions are first-class citizens in JavaScript and are an object.
- Functions can be passed around like any other object.
- Functions can return functions.
() ⇒ () ⇒ () ⇒ console.log("FUNCTION")
- Functions can be passed to another function
[1,2].map(() => console.log('hoi'))
- Functions can be assigned to objects
{a: function() { console.log('jello') }
- When a function is assigned to an object, it’s
this
points to the object. - Functions have a
.call
method which can be used to set thethis
context. - Arrow functions without
{}
return without thereturn
keyword. - Arrow functions don’t have a
this
context. Theirthis
would refer to the parent scope. - Arrow functions don’t have a prototype property.
Prototypal Inheritance (Functional)
Inheritance is achieved with a chain of prototypes.
For functional approach use
Object.create
. It takes an object and an property descriptor.Use
Object.getOwnPropertyDescriptor(obj, 'key')
to get property descriptor on any object.A descriptor can have:
get, set, writable, enumerable, and configurable
. Default values are false.const wolf = { howl: function() { console.log(this.name + ': awooooooo') } } const dog = Object.create(wolf, { woof: { value: function() { console.log(this.name + ': woof') } } }) function createDog(name) { return Object.create(dog, { name: {value: name + ' the dog'} }) } const rufus = createDog('Rufus'); rufus.woof(); rufus.howl(); console.log(Object.getPrototypeOf(rufus) === dog)) console.log(Object.getPrototypeOf(dog) === wolf))
Prototypal Inheritance (Constructor Functions)
- Objects with a specific prototype can be achieved with the
new
keyword as well. - Here you define properties on a function’s prototype and then call that function with
new
- The function name is capitalized when being used with the new keyword.
- We can inherit using:
- a temporary empty constructor function.
Object.create
.- or
utils.inherit
from node core. - Object.setPrototypeOf(Dog.prototype, Wolf.Prototype);
function Wolf(name) { this.name = name; } Wolf.prototype.howl = function() { console.log(this.name + ': awoooo'); } function Dog(name) { Wolf.call(this, name + ' the dog'); } function inherit(proto) { function ChainLink() {}; ChainLink.prototype = proto; return new ChainLink(); } Dog.prototype = inherit(Wolf.prototype); Dog.prototype = Object.create(Wolf.prototype); require('utils');.inherit(Dog, Wolf); Object.setPrototypeOf(Dog.prototype, Wolf.prototype); Dog.prototype.woof = function() { console.log(this.name + ': woof'); } const rufus = new Dog('Rufus'); rufus.woof(); rufus.howl(); console.log(Object.getPrototypeOf(rufus) === Dog.prototype)); console.log(Object.getPrototypeOf(Dog.prototype) === Wolf.prototype));
- Objects with a specific prototype can be achieved with the
Prototypal Inheritance (Class-syntax Constructors)
- ES2015 introduced
class
es to JavaScript. However, it isn’t the same as classical inheritance. - classes are syntactic sugar and use functions internally that would be called with new.
class Foo{}; typeof foo
- class syntax reduce boilerplate when creating prototype chain
- Inheritance happens using the
extend
keyword. This ensures the prototype of the class points to the ones it is extending super
is a generic way to call the parent constructor while settingthis
to the current instance.- Methods defined on the class are added to the object prototype
class Wolf { constructor(name) { this.name = name; } howl() { console.log( this.name + ': awooooo'); } } class Dog extends Wolf { constructor(name) { super(name + ' the dog'); } woof() { console.log(this.name + ': woof') } } const rufus = new Dog('Rufus'); rufus.woof(); rufus.howl(); console.log(Object.getPrototypeOf(rufus) === Dog.prototype); console.log(Object.getPrototypeOf(Dog.prototype) === Wolf.prototype);
- ES2015 introduced
Closure Scope
- Every time a function is created there is a scope created.
- Parameters and variables created in the function are stored in this invisible object (of scope).
- Function within a function can access both its own closure scope and parent scope of the outer functions.
- Closure scope can’t be accessed from outside.
- Function returning a function can still access parent scope. This can help with private encapsulation and state.
- We can emulate inheritance using closures. However, there are no prototypes involved. There is no
this
involved! - The downside of such inheritance is that the code is copied everytime.
function outerFn() { var foo = true; function print() { console.log(foo) } print(); foo = false; print(); } outerFn(); function init(type) { let id = 0; return (name) => { id += 1; return { id, type, name} } } const a = init('user'); // 1 const b = init('book'); // 2 function wolf(name) { const howl = () => { console.log(name + ': awoooo'); } return {howl} } function dog(name) { name = name + ' the dog'; const woof = () => {console.log(name + ': woof') } return {...wolf(name), woof} } const rufus = dog('rufus'); rufus.woof(); rufus.howl();
Lab check
function prefixer(prefix) {
return function(message) {
return `${prefix} ${message};
}
}
const hi = prefixer("Hi, ");
hi("Bassam");
hi("Baheej");
const assert = require('assert');
const H = {
hiss: () => {}
}
const P = Object.create(H, {
prrr: {
value: () => {}
}
});
const M = Object.create(P, {
meow: {
value: () => {}
}
});
const felix = Object.create(M);
function H() {}
H.prototype.hiss = () =>{}
function P() {
H.call(this)
}
P.prototype.prrr = () =>{}
Object.setPrototypeOf(P.prototype, H.prototype);
function M() {
P.call(this)
}
M.prototype.meow= () =>{}
Object.setPrototypeOf(M.prototype, P.prototype);
function H() {
function hiss() {}
return {hiss}
}
function P() {
function prrr() {};
return { ...H(), prrr}
}
function M() {
function meow() {}
return {...P(), meow}
}
const felix = new M();
felix.meow()
felix.prrr()
felix.hiss()
const felixProto = Object.getPrototypeOf(felix);
const felixProtoProto = Object.getPrototypeOf(felixProto);
const felixProtoProtoProto = Object.getPrototypeOf(felixProtoProto);
assert(Object.getOwnPropertyNames(felixProto).length, 1);
assert(Object.getOwnPropertyNames(felixProtoProto).length, 1);
assert(Object.getOwnPropertyNames(felixProtoProtoProto).length, 1);
assert(typeof felixProto.meow, 'function');
assert(typeof felixProtoProto.prrr, 'function');
assert(typeof felixProtoProtoProto.hiss, 'function');
console.log('All check passed');
Knowledge Check
- what is
this
when a function on objA is called on objB:objB
- what does extend do:
sets up part of a prototype chain
- closure scope can be accessed from:
inside function and any functions within that function
Packages & Dependencies
npm
binary is installed with Node.- By default npm points to the npmjs.com registry.
- To get more details and commands related to
npm
runnpm help
. - To get help related to a particular command run
npm install --help
- A npm package is a folder with
package.json
file in it. A node application or service is also a package as they too have apackage.json
. - To generate a
package.json
file usenpm init
and answer the interactive questions. npm init
will create a package.json in the folder it is in. The name of the package would be based on the directory name.- For
npm init
to use the default values usenpm init -y
. - Default fields generated in package.json are:
name, version, description, main, scripts, keywords, author, license
. - On running
npm init
command in the directory again, it will update the package.json file. Also addinggit
repository details. - To install dependencies run
npm install pino
.pino
would be added a dependency to package.json underdependenies
object. dependencies
contains the key which is the dependency namespace and the value is a semver range version number.- Running
npm install pino
will install the latest version as a specific version isn’t provided. - All the dependencies are installed in the
node_modules
directory. npm install
uses a maximally flat strategy where all packages in a dependency tree are placed at the top level unless there are two different version. In which case the package store those versions in a nestednode_modules
directory.- Since modules are listed in the
package.json
file, thenode_modules
directory can be deleted. The packages can be installed again usingnpm install
. node_modules
directory shouldn’t be checked into the version control system.npm ls
only shows thedependencies
and skips thedevDependencies
.- To install a
devDependency
runnpm install --save-dev standard
ornpm i -D standard
. This will installstandard
indevDependencies
section of package.json npm ls
will now show both dependencies and devDependencies.- To just install
dependencies
runnpm install --production
- Semver format is three number separated by a dot: major.minor.patch
- Semver range gives the flexibility to install packages within a range
- 1.x.x any minor or patch version
- 1.1.x any patch version
- By default packages are installed with a ^ (^1.2.3). It translates to 1.x.x
- ^0.0.0 could mean x.x.x because of the 0
scripts
in package.json can be used to create aliases for shell commands.scripts
automatically pick up binaries from thenode_modules/bin
directory.scripts: { lint: eslint }
and to run this usenpm run lint
.start
andtest
scripts are aliases asnpm start or npm s
andnpm test or npm t
.
Lab check
npm install --save-dev nonsynchronous
npm install [email protected] && npm install --save-exact [email protected]
Knowledge Check
- covered in a Semver range of ^2.1.2?:
2.14.2, 2.16.1, 2.14.4
- how to run test and lint scripts:
npm run lint && npm test
- when will a packages devDependencies be installed: never
Node’s Module System
- To import a module:
const pino = require('pino')
require
looks for a module in thenode_modules
directory and return the exported value from themain
file listed inpackage.json
of the module.require
will return whatever is generated from the module not just functions.module.exports
anything assigned to it will be exported on require.- If relative files are required then they won’t be looked up in the
node_modules
directory.require('./format)
- We can export a module as a binary or library or even both.
- When a file is an entry point, then it is the main module. We can check that using
require.main === module
- To determine the absolute path for any required module you can use
require.resolve
. - For core modules the path would just be the module name.`
Lab check
module.exports = function (a, b) {
return a + b;
}
const add = require('../labs-1');
console.log(add(19,23));
Knowledge Check
- If there is a package foo and a file
./foo.js
which file wouldrequire('foo')
load?: main file from the package. Not the index necessarily. - require.resolve: returns the path to a module.
- How to export function
func
from a file:module.export = func
Asynchronous Control Flow
- Callbacks
- A function that will be called sometime in future once a task has been completed
- Node has a convention of return err and value in the callback, know as Errback.
- To have synchronous flow using callbacks, we can nest them
- Promises
- is an object that represents an async operation.
- it can be either pending or settled (resolved or rejected)
- instead of being called like callbacks, promise return a value when complete.
- You can use promisify from utils to convert callbacks to promises
- Promises can be handled by chaining
then
andcatch
methods. then
can allow serial execution as each return a promise- for async execution we can use
allSettled
orall
or invidiualthen
- Async/Await
- makes the promise code look syncronous
- async function always returns a promise
- await can only be used inside async function to wait for a promise to settle
- we can use await in a for loop to have sequential loops
- at times using callback is less complex that promises or async/await
Lab check
opA(print)
opB(print)
opC(print)
const pa = promisify(opA);
const pb = promisify(opB);
const pc = promisify(opC);
pa().then((err, res) => {
print(err, res);
pb().then((err, res) => {
print(err, res);
pc().then((err, res) => {
print(err, res);
})
})
})
Knowledge Check
- callback: function that is called when async operations completes
- promise rejection: catch
- async always return: a promise