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 12andnvm use 12.
Lab check
- Check the node version. Use
node -v. A number prefixed withvshould 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
nodebinary.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.jsornode -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.jsornode -r preload.js app.js. - The default stack trace has 10 stack frames.
- Stack trace limit can be increased using
--stack-trace-limitflag 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
debuggerkeyword. It should be removed after use.
Lab check
node --inspect-brk app.jsand enable Pause on caught exceptions.- Add
debuggeron the first line of functionf.
Knowledge Check
debuggerto set a breakpoint using code.--inspect-brkto 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.
.formethod 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
thispoints to the object. - Functions have a
.callmethod which can be used to set thethiscontext. - Arrow functions without
{}return without thereturnkeyword. - Arrow functions don’t have a
thiscontext. Theirthiswould 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
newkeyword 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.inheritfrom 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
classes 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
extendkeyword. This ensures the prototype of the class points to the ones it is extending superis a generic way to call the parent constructor while settingthisto 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
thisinvolved! - 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
thiswhen 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
npmbinary is installed with Node.- By default npm points to the npmjs.com registry.
- To get more details and commands related to
npmrunnpm help. - To get help related to a particular command run
npm install --help - A npm package is a folder with
package.jsonfile in it. A node application or service is also a package as they too have apackage.json. - To generate a
package.jsonfile usenpm initand answer the interactive questions. npm initwill create a package.json in the folder it is in. The name of the package would be based on the directory name.- For
npm initto 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 initcommand in the directory again, it will update the package.json file. Also addinggitrepository details. - To install dependencies run
npm install pino.pinowould be added a dependency to package.json underdependeniesobject. dependenciescontains the key which is the dependency namespace and the value is a semver range version number.- Running
npm install pinowill install the latest version as a specific version isn’t provided. - All the dependencies are installed in the
node_modulesdirectory. npm installuses 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_modulesdirectory.- Since modules are listed in the
package.jsonfile, thenode_modulesdirectory can be deleted. The packages can be installed again usingnpm install. node_modulesdirectory shouldn’t be checked into the version control system.npm lsonly shows thedependenciesand skips thedevDependencies.- To install a
devDependencyrunnpm install --save-dev standardornpm i -D standard. This will installstandardindevDependenciessection of package.json npm lswill now show both dependencies and devDependencies.- To just install
dependenciesrunnpm 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
scriptsin package.json can be used to create aliases for shell commands.scriptsautomatically pick up binaries from thenode_modules/bindirectory.scripts: { lint: eslint }and to run this usenpm run lint.startandtestscripts are aliases asnpm start or npm sandnpm test or npm t.
Lab check
npm install --save-dev nonsynchronousnpm 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') requirelooks for a module in thenode_modulesdirectory and return the exported value from themainfile listed inpackage.jsonof the module.requirewill return whatever is generated from the module not just functions.module.exportsanything assigned to it will be exported on require.- If relative files are required then they won’t be looked up in the
node_modulesdirectory.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.jswhich 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
funcfrom 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
thenandcatchmethods. thencan allow serial execution as each return a promise- for async execution we can use
allSettledorallor 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