Skip to main content

GraalVM's Nodejs

It's absolutely refreshing to witness how the JVM can be bent to fit in places where hitherto it hasn't been able to be competitive. A case in point is the evolution of its Javascript engine from Rhino to a vastly improved Nashorn, and now to a completely re-engineered Nodejs implementation in GraalVM. Let's have some fun and see how far we can get with using this latest incarnation of the JVM's Javascript engine

Install GraalVM#

Head over to the Oracle website to grab a fresh copy of GraalVM, and unzip it to a folder of your choice. Since we are just evaluating GraalVm, download the Enterprise edition for Java 11. There's a version 16 but I will stay with version 11 here. Unpack this bundle into your folder of choice, and create a GRAALVM_HOME environment variable pointing to this folder

Now install the node js implementation of GraalVM to yout GraalVM installation. On windows, navigate to GRAAALVM_HOME from the CMD terminal and execute the command below

.\bin\gu.cmd install nodejs

For good measure, install the native-image utility to your GraalVM installation, if your OS supports it. On windows, navigate to GRAAALVM_HOME from the CMD terminal and execute the command below

.\bin\gu.cmd install native-image

Verify that you have a valid nodejs installation

%GRAALVM_HOME%\bin\node.cmd -vv14.16.1

Create an NPM project#

As usual, create an empty folder ain a location of your choices, nd in this folder initialize an npm project. We will use npm to manage node dependencies, but use Graal's nodejs as the Javascript engine

mkdir js-as-javacd js-as-javanpm init -y

Start an Express js application#

Now start putting together a barebone Express js application

npm i express -S

Now create an index.js to the project folder file and add this content

const express = require('express')const app = express()
app.get('/', (req, res) => {  res.send('Hello World!')})
const port = 3003app.listen(port, () => {  console.log(`Express app on http://localhost:${port}`)})

Add a script in package.json to start the web server. At this point, your package.json should have the elements shown below

..."main": "index.js","scripts": {  "start:server": "%GRAALVM_HOME%\bin\node.cmd index"},"dependencies": {  "express": "^4.17.1"},...

Giving it a first spin#

Now let's see if GraalVM's node implementation can run the same application

npm run start:serverjs-as-java@1.0.0 start:server <this should match your project folder>> %GRAALVM_HOME%\bin\node.cmd index
Example app listening at http://localhost:3003

And just like that, we are running express using GraalVM's node implementation

Let's add some arithmetic functions#

Now extend the application to serve basic arithmetic operations (plus, minus, times, divide). Add a src/service folder in the project folder and add a new file calc.js

function plus(left, right) {    return Number(left) + Number(right)}
function minus(left, right) {    return Number(left) - Number(right)}
function times(left, right) {    return Number(left) * Number(right)}
function divide(left, right) {    return Number(left) / Number(right)}
module.exports = { plus, minus, times, divide }

Add a handler module#

Create a new folder src/handler folder and in the handler folder, create a new file index.js

const { plus, minus, times, divide } = require('../service/calc')
const plusHandler = async (req, res) => {    const { left, right } = req.params;    try {        const sum = plus(left, right)        console.log(`${left} + ${right} = ${sum}`)        res.send({ data: sum })    }    catch (err) {        res.status(503).send(err.message)    }}
const minusHandler = async (req, res) => {    const { left, right } = req.params;    try {        const diff = minus(left, right)        console.log(`${left} - ${right} = ${diff}`)        res.send({ data: diff })    }    catch (err) {        res.status(503).send(err.message)    }}
const timesHandler = async (req, res) => {    const { left, right } = req.params;    try {        const mult = times(left, right)        console.log(`${left} * ${right} = ${mult}`)        res.send({ data: mult })    }    catch (err) {        res.status(503).send(err.message)    }}
const divideHandler = async (req, res) => {    const { left, right } = req.params;    try {        const quot = divide(left, right)        console.log(`${left} / ${right} = ${quot}`)        res.send({ data: quot })    }    catch (err) {        res.status(503).send(err.message)    }}module.exports = { plusHandler, minusHandler, timesHandler, divideHandler }

Add a routes module#

Create a new folder src/routes folder and in the routes folder, create a new file index.js

const router = require('express').Router()const {    plusHandler,    minusHandler,    timesHandler,    divideHandler} = require('../handler')
router.get('/plus/:left/:right', plusHandler);router.get('/minus/:left/:right', minusHandler);router.get('/times/:left/:right', timesHandler);router.get('/divide/:left/:right', divideHandler);
module.exports = router

Modify the index module#

Before giving the application a second spin, let's make some slight adjustments

const express = require('express')const router = require('./src/routes')
const app = express()app.use(router)
const port = 3003...

Fire up the application again, and just like that, it works seemlessly. This is already light-years better than Nashorn

What about unit testing?#

Now is a good opportunity to turn the heat up and sprinkle some tests. Time to bring out jest.

npm i -D jest

Now initialize jest

node .\node_modules\jest\bin\jest.js --init
The following questions will help Jest to create a suitable configuration for your project
โˆš Would you like to use Jest when running "test" script in "package.json"? ... yesโˆš Would you like to use Typescript for the configuration file? ... noโˆš Choose the test environment that will be used for testing ยป nodeโˆš Do you want Jest to add coverage reports? ... yesโˆš Which provider should be used to instrument code for coverage? ยป babelโˆš Automatically clear mock calls and instances between every test? ... no

Spin up some tests fro the service module. Create a test folder at the same level as the src

mkdir -p src/__tests__/service

Inside the service folder in tests, add a new file - index.test.js

const { plus, minus, times, divide } = require('../../src/service/calc')
describe('Testing simple arithmetic ops', () => {
    test('plus should yield the sum of two numbers', () => {        const sum = plus(3, 4)        expect(sum).toBe(7)    })
    test('minus should yield the differenrce bwteen two numbers', () => {        const diff = minus(3, 4)        expect(diff).toBe(-1)    })
    test('times should yield the product of two numbers', () => {        const product = times(3, 4)        expect(product).toBe(12)    })
    test('divide should yield the quotient of two numbers', () => {        const quot = divide(3, 4)        expect(quot).toBe(0.75)    })})

Add a test script to execute these tests. The equivalent using nodejs would simply be "test": "jest"

"test": "%GRAALVM_HOME%\\bin\\node.cmd .\\node_modules\\jest\\bin\\jest.js"

After a brief moment, which almost feels like eternity, GraalVM's nodejs executes the tests without batting an eyelid

npm run test
> js-as-java@1.0.0 test <this should match your project folder>> %GRAALVM_HOME%\bin\node.cmd .\node_modules\jest\bin\jest.js
 PASS  __tests__/service/index.test.js  Testing simple arithmetic ops    โˆš plus should yield the sum of two numbers (6 ms)    โˆš minus should yield the differenrce bwteen two numbers (3 ms)    โˆš times should yield the product of two numbers (2 ms)    โˆš divide should yield the quotient of two numbers (3 ms)
----------|---------|----------|---------|---------|-------------------File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s----------|---------|----------|---------|---------|-------------------All files |     100 |      100 |     100 |     100 | calc.js  |     100 |      100 |     100 |     100 |----------|---------|----------|---------|---------|-------------------Test Suites: 1 passed, 1 totalTests:       4 passed, 4 totalSnapshots:   0 totalTime:        6.286 sRan all test suites.

So far, things look amazing. Now the next article, I'll turn up the heat even higher and see how we can tap into both worlds of Java and NodeJS using the same GraalVM software