NodeJS

How to install NodeJS 20.x LTS on Ubuntu in 1 minute

Run these shell commands on your Ubuntu computer to install NodeJS 20.x:

sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg

NODE_MAJOR=20
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list

sudo apt-get update
sudo apt-get install nodejs -y

 

Instead of 20 you can also choose other versions like 18. However, using this method, you can’t install multiple versions of NodeJS in parallel.

Source: Official nodesource documentation

Posted by Uli Köhler in Linux, NodeJS

Which tsc –target to use for different NodeJS versions?

Depending on the minimum NodeJS version you intend to support,, you should use the following –target settings for tsc:

  • Node 10.x: es2018
  • Node 12.x: es2019
  • Node 14.x: es2020
  • Node 16.x: es2021
  • Node 17.x: es2022
  • Node 18.x: es2022
  • Node 19.x: es2022
  • Node 20.x: es2022

Note that there are more configurations possible than just --target when compiling for NodeJS. See the tsconfig base config examples on Github for more details.

Source: The tsconfig bases on Github.

Posted by Uli Köhler in NodeJS, Typescript

NodeJS TCP ping example (Typescript)

The following Typescript example allows you to “ping” an IP address or host name not using ICMP but using TCP connection to a given port. If the TCP connection is accepted, the ping is resolved as true. If no connection can be established, it is returned as false. In any case, no data is exchanged and the connection is closed immediately after establishing it.

import { Socket } from "net";

/**
 * Basic TCP ping that returns true if the connection is successful, false if it fails
 * The socket is closed after the connection attempt, no data is exchanged.
 */
export function TCPConnectPing(ipAddress, timeout=5000, port=80): Promise<boolean> {
    return new Promise((resolve) => {
      const socket = new Socket();
      let connected = false;
  
      // Set a timeout for the connection attempt
      const timer = setTimeout(() => {
        if (!connected) {
          socket.destroy();
          resolve(false); // Connection timed out
        }
      }, timeout);
  
      socket.connect(port, ipAddress, () => {
        clearTimeout(timer);
        connected = true;
        socket.destroy();
        resolve(true); // Connection successful
      });
  
      socket.on('error', (error) => {
        clearTimeout(timer);
        if (!connected) {
          resolve(false); // Connection failed due to error
        }
      });
    });
  }

 

Posted by Uli Köhler in Networking, NodeJS

How to fix NodeJS / NPM “TypeError: Class extends value undefined is not a constructor or null” in /usr/lib/node_modules/npm/node_modules

Problem:

When you try to run any npm command such as npm start , you see an error message such as

/usr/lib/node_modules/npm/lib/es6/validate-engines.js:31
    throw err
    ^

TypeError: Class extends value undefined is not a constructor or null
    at Object.<anonymous> (/usr/lib/node_modules/npm/node_modules/fs-minipass/lib/index.js:136:4)
    at Module._compile (node:internal/modules/cjs/loader:1233:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1287:10)
    at Module.load (node:internal/modules/cjs/loader:1091:32)
    at Module._load (node:internal/modules/cjs/loader:938:12)
    at Module.require (node:internal/modules/cjs/loader:1115:19)
    at require (node:internal/modules/helpers:119:18)
    at Object.<anonymous> (/usr/lib/node_modules/npm/node_modules/cacache/lib/content/read.js:4:13)
    at Module._compile (node:internal/modules/cjs/loader:1233:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1287:10)

Solution:

This is an internal error in npm or packages required by npm. Typically, it is caused by an incomplete update of NodeJS and npm which leaves npm or its dependencies in a broken state.

The solution is to reinstall NodeJS:

  • Uninstall NodeJS using a command such as
    sudo apt remove nodejs
  • Remove the NodeJS package folder entirely, on Linux it can be moved to a backup location using e.g.
    sudo mv /usr/lib/node_modules ~/node_modules_backup
  • Re-installing NodeJS using a command such as
    sudo apt -y install nodejs
  • Re-installing any globally installed packages you need, such as npm i -g @angular/cli
Posted by Uli Köhler in NodeJS

Do Docker “node” images have ssh-keygen?

I tested with node:20 and node:20-alpine and can confirm that:

  • node:20 (i.e. debian bookworm based) has a working ssh-keygen
  • node:20-alpine does not have ssh-keygen

On Alpine, you can install ssh-keygen using apk.

Typically, this means you have to build your own docker image based no node:20-alpine. In your Dockerfile, add

RUN   apk update && \
      apk add --no-cache \
      openssh-keygen

 

Posted by Uli Köhler in Docker, NodeJS

NodeJS MikroTik PoE status query example

This example builds on our previous posts NodeJS Mikrotik API minimal example and MikroTik RouterOS: How to power-cycle PoE using the terminal.

The following code will print the PoE status on Port ether5 on the given MikroTik device using the MikroTik API.

import * as MikroNode from 'mikrotik' ;

const host = "192.168.88.1";
const username = "admin";
const password = "admin1234"; // Hope that's not your real password ;)

const connection = MikroNode.getConnection(host, username, password, {
    closeOnDone : true
});

connection.getConnectPromise().then(function(conn) {
    conn.getCommandPromise(['/interface/ethernet/poe/print', '?name=ether5']).then(values => {
        console.log(values);
    }, reason => {
        console.log('Error while running command: ' + JSON.stringify(reason));
    });
}).catch(reason =>  {
    console.log('Error while connecting: ' + JSON.stringify(reason));
});

Example output:

[
  {
    '.id': '*5',
    name: 'ether5',
    'poe-out': 'forced-on',
    'poe-priority': '10',
    'poe-lldp-enabled': 'false',
    'power-cycle-ping-enabled': 'false',
    'power-cycle-interval': 'none',
    '.about': 'poe-out status: power_reset'
  }
]

If the PoE is currently being power-cycled, this will print:

[
  {
    '.id': '*5',
    name: 'ether5',
    'poe-out': 'forced-on',
    'poe-priority': '10',
    'poe-lldp-enabled': 'false',
    'power-cycle-ping-enabled': 'false',
    'power-cycle-interval': 'none',
    '.about': 'poe-out status: power_reset'
  }
]

 

Posted by Uli Köhler in MikroTik, NodeJS, PoE

NodeJS Mikrotik API minimal example

This is an example of access the Mikrotik API using NodeJS and the mikrotik package.

First, install the package

npm i --save mikrotik

Also, in order to enable import statement, set

"type": "module"

in package.json.

Example code:

import * as MikroNode from 'mikrotik' ;

const host = "10.56.23.4";
const username = "admin";
const password = "N@CdVTz8y@D$KwVS5TTo"; // Hope that's not your real password ;)

const  connection = MikroNode.getConnection(host, username, password, {
    closeOnDone : true
});

connection.getConnectPromise().then(function(conn) {
    conn.getCommandPromise('/ip/address/print').then(addresses => {
        for(const address of addresses) {
            console.info(`Address: ${address.address} on ${address.interface}`);
        }
    }, reason => {
        console.log('Error while running command: ' + JSON.stringify(reason));
    });
}).catch(reason =>  {
    console.log('Error while connecting: ' + JSON.stringify(reason));
});

This will output, for example:

Address: 192.168.88.1/24 on bridge
Address: 10.1.2.3/24 on bridge

In case of bad username/password credentials, it will print:

Error while connecting: {"errors":[{"category":"","message":"invalid user name or password (6)"}],"channelId":"login","channel":{"id":"login","running":true,"closing":true,"closed":true,"clearEvents":false,"saveBuffer":true,"closeOnDone":false,"lastCommand":["/login","=name=admin","=password=admin1234",".tag=login"],"_events":{},"_eventsCount":0}}

 

Posted by Uli Köhler in MikroTik, NodeJS

How to generate SSH public & private key in NodeJS using ssh2 library

The following code is a Promise based wrapper around ssh2’s utils.generateKeyPair():

const { utils: { generateKeyPair } } = require('ssh2');
const {writeFile} = require('fs/promises');

/**
 * ssh2's generateKeyPair as promise
 */
function SSHGenerateKeyPairPromise(keytype="ed25519", opts = {}) {
    return new Promise((resolve, reject) => {
        generateKeyPair(
            keytype, opts,
            (err, keys) => {
                if (err) { return reject(err) };
                return resolve(keys);
            }
        );
    });
}

/**
 * Run SSHGenerateKeyPairPromise() and save the keys to
 * opts.privateKeyPath and opts.publicKeyPath
 * @returns The generated keys
 */
async function SSHGenerateAndSaveKeyPairPromise(keytype="ed25519", opts = {}) {
    // Check if opts.privateKeyPath and opts.publicKeyPath are set
    if (!opts.privateKeyPath || !opts.publicKeyPath) {
        throw new Error("opts.privateKeyPath and opts.publicKeyPath must be set");
    }
    const keys = await SSHGenerateKeyPairPromise(keytype, opts);
    //Save keys to opts.privateKeyPath and opts.publicKeyPath using fs.promise
    await writeFile(opts.privateKeyPath, keys.private);
    await writeFile(opts.publicKeyPath, keys.public);
    return keys;
}

Usage example for ed25519 keys:

SSHGenerateAndSaveKeyPairPromise('ed25519', {
    privateKeyPath: 'id_ed25519',
    publicKeyPath: 'id_ed25519.pub'
}).then(keys => {
    console.log("Successfully generated ed25519 keys");
}).catch(err => {
    console.log(err);
});

Usage example for rsa keys:

SSHGenerateAndSaveKeyPairPromise('rsa', {
    bits: 8192,
    privateKeyPath: 'id_rsa',
    publicKeyPath: 'id_rsa.pub'
}).then(keys => {
    console.log("Successfully generated RSA keys");
}).catch(err => {
    console.log(err);
});

 

Posted by Uli Köhler in NodeJS

How to fix NodeJS argon2 crash

My NodeJS app using the argon2 library crashed within my docker container (the process just exited without an error message) every time I used argon2 funtions, even though it worked fine on my Desktop.

This error occured with argon2==0.30.3I fixed it by downgrading it to 0.26.2 using the following statement in package.json:

"argon2": "^0.26.2",

With the correct version installed (don’t forget to npm install after changing package.json), the issue disappeared entirely. I did not try any other versions of argon2.

Posted by Uli Köhler in NodeJS

What is the NodeJS equivalent of Python’s if name == “__main__”

Whereas in Python you would use

if name == "__main__":
    # TODO Your code goes here

in NodeJS you can simply use

if (require.main === module) {
    // TODO Your code goes here
}

 

Posted by Uli Köhler in Javascript, NodeJS

How to set Koa HTTP response code

In order to set the HTTP response code using Koa, use ctx.status = ...

ctx.status = 403;

Full example:

const Koa = require("koa");

const app = new Koa();

app.use(async (ctx, next) => {
  ctx.status = 403;
  ctx.body = "Forbidden";
});

app.listen(3000);

 

Posted by Uli Köhler in Javascript, NodeJS

How to fix docker NodeJS container not shutting down

When you are running a NodeJS server inside a docker container, you will often encouter the issue that the container does not shutdown properly but taking a long time (several minutes) to shutdown.

In order to fix this, add the following code to your main NodeJS file (typically you should add it at the top of the file to make sure nothing prevents it from getting executed)

process.on('SIGTERM', function() {
  console.log('SIGTERM received, shutting down...');
  process.exit(0);
});
process.on('SIGINT', function() {
  console.log('SIGINT received, shutting down...');
  process.exit(0);
});

This will force NodeJS to exit (more or less immediately) once either SIGINT or SIGTERM is received. Typically, Docker sends SIGTERM on container shutdown.

Background information

See this GitHub guide

Posted by Uli Köhler in NodeJS

How to fix NodeJS Error: EXDEV: cross-device link not permitted, rename …

Problem:

While trying to rename a file using fs.move() in NodeJS, you see an error message such as

Error: EXDEV: cross-device link not permitted, rename '/tmp/upload_8a8c71784abf9942b40c1359935b1997' -> 'myfile.pdf'

Solution:

This error occurs because you are moving a file from one drive (/tmp in our example above) to another drive (the current directory in our example above). On most platforms, this can’t be done using a simple rename operation.

Instead, fs.copyFile() the file.

For example, instead of

await fs.move('/tmp/upload_8a8c71784abf9942b40c1359935b1997', 'myfile.pdf')

first, copy the file:

await fs.copyFile('/tmp/upload_8a8c71784abf9942b40c1359935b1997', 'myfile.pdf')

and then – if desired – remove the source file to mirror the behaviour of os.move() as closely as possible.

await fs.remove('/tmp/upload_8a8c71784abf9942b40c1359935b1997', 'myfile.pdf')
await fs.unlink('/tmp/upload_8a8c71784abf9942b40c1359935b1997')

 

Posted by Uli Köhler in NodeJS

How to get filesize in NodeJS / TypeScript using Promisese

First, import stat from the NodeJS standard library:

import { stat } from "node:fs/promises";

Async-await style

// Async-await style:
const statResult = await stat("myfile.pdf");
const fileSizeInBytes = statResult.size;

Promise.then() style

stat("myfile.pdf").then(statResult => {
    const fileSizeInBytes = statResult.size;
    // TODO your code goes here
});

 

Posted by Uli Köhler in NodeJS, Typescript

Simple tsconfig.json template for NodeJS

Use this tsconfig.json as a starting point to setup your NodeJS TypeScript project:

{
  "compilerOptions": {
    "strict": false,
    "target": "es2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "outDir": "./build",
    "rootDir": "./",
    "lib": [
      "esnext"
    ],
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "**/*.ts",
    "types/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}

 

Posted by Uli Köhler in NodeJS, Typescript

How to import @koa/router in Typescript

import Router from '@koa/router';
import Koa from 'koa';

const app = new Koa();
const router = new Router();

 

Posted by Uli Köhler in NodeJS, Typescript

How to import Koa in Typescript

import Koa from 'koa';

const app = new Koa();

/* TODO your code goes here */

 

Posted by Uli Köhler in NodeJS, Typescript

Minimal Koa Typescript example using @koa/router and koa-body

This example showcases how to make a simple Hello World server using Koa in Typescript.

import Router from '@koa/router';
import koaBody from 'koa-body';
import Koa from 'koa';

const router = new Router();
const app = new Koa();

app.use(koaBody());

router.get('/', (ctx)  => {
    ctx.body = "Hello world";
});

app
    .use(router.routes())
    .use(router.allowedMethods());

if (!module.parent) app.listen(3000);
{
  "compilerOptions": {
    "strict": true,
    "target": "es2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "outDir": "./build",
    "rootDir": "./",
    "lib": [
      "esnext"
    ],
    "forceConsistentCasingInFileNames": true
  },
  "include": [
    "**/*.ts",
    "types/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}
{
  "name": "KoaTypescriptTest",
  "version": "1.0.0",
  "description": "",
  "scripts": {
    "start": "./node_modules/.bin/tsnd --respawn server.ts",
    "build": "./node_modules/.bin/tsc -b tsconfig.json"
  },
  "dependencies": {
    "@koa/router": "^12.0.0",
    "koa": "^2.14.1",
    "koa-body": "^6.0.1"
  },
  "devDependencies": {
    "@types/koa": "^2.13.5",
    "@types/koa__router": "^12.0.0",
    "@types/node": "^18.11.17",
    "ts-node-dev": "^1.1.8",
    "typescript": "^4.9.4"
  }
}

 

Posted by Uli Köhler in NodeJS, Typescript

EMQX 5.x HTTP ACL server using NodeJS

In our previous post EMQX 5.x HTTP password authentication server minimal example using NodeJS we provided a complete example of how to implement EMQX HTTP authentication.

This post provides an extension to our previous HTTP auth by adding a Koa router (i.e. a HTTP endpoint / URL) to provide ACL authentication, i.e. allow or deny topic level access with custom logic.

router.post('/emqx/acl', async ctx => {
    const body = ctx.request.body;
    console.log(body)
    // TODO: This example always returns true
    // You need to implement your authentication logic
    ctx.body = {
        result: "allow",
    };
});

Add that code before app.use(router.routes()); in the script from EMQX 5.x HTTP password authentication server minimal example using NodeJS.

My recommended Authorization configuration body which you can set in the EMQX dashboard is

{
  "action": "${action}",
  "client_id": "${clientid}",
  "ip": "${peerhost}",
  "topic": "${topic}",
  "username": "${username}"
}

 

Posted by Uli Köhler in EMQX, MQTT, NodeJS