Node.js: Calling a REST backend service from an Express application

Now that we’ve seen how to make HTTP client requests, we can look at how to make a REST query within an Express web application. What that effectively means is making an HTTP GET request to a backend server, which responds to the Fibonacci number represented by the URL. To do so, we’ll refactor the Fibonacci application to make a Fibonacci server that is called from the application. While this is overkill for calculating Fibonacci numbers, it lets us see the basics of implementing a multi-tier application stack in Express.

Inherently, calling a REST service is an asynchronous operation. That means calling the REST service will involve a function call to initiate the request and a callback function to receive the response. REST services are accessed over HTTP, so we’ll use the HTTPClient object to do so. We’ll start this little experiment by writing a REST server and exercising it by making calls to the service. Then, we’ll refactor the Fibonacci service to call that server.

1. Implementing a simple REST server with Express

While Express can also be used to implement a simple REST service, the parameterized URLs we showed earlier (/user/profile/:id) can act like parameters to a REST call. Express makes it easy to return data encoded in JSON format.

Now, create a file named fiboserver.js, containing the following code:

const math = require(‘./math’);

const express = require(‘express’);

const logger = require(‘morgan’);

const app = express();

app.use(logger(‘dev’));

app.get(‘/fibonacci/:n’, (req, res, next) => {

math.fibonacciAsync(Math.floor(req.params.n), (err, val) => {

if (err) next(`FIBO SERVER ERROR ${err}`); else {

res.send({

n: req.params.n, result: val

});

}

});

});

app.listen(process.env.SERVERPORT);

This is a stripped-down Express application that gets right to the point of providing a Fibonacci calculation service. The one route it supports handles the Fibonacci computation using the same functions that we’ve already worked with.

This is the first time we’ve seen res.send used. It’s a flexible way to send responses that can take an array of header values (for the HTTP response header) and an HTTP status code. As used here, it automatically detects the object, formats it as JSON text, and sends it with the correct Content-Type parameter.

In package.json, add the following to the scripts section:

“server”: “cross-env SERVERPORT=3002 node ./fiboserver”

This automates launching our Fibonacci service.

Now, let’s run it:

$ npm run server

> fibonacci@0.0.0 server /Users/David/chap04/fibonacci

> cross-env SERVERPORT=3002 node ./fiboserver 

Then, in a separate command window, we can use the curl program to make some requests against this service:

$ curl -f http://localhost:3002/fibonacci/10

{“n”:”10″,”result”:55}

$ curl -f http://localhost:3002/fibonacci/11

{“n”:”11″,”result”:89}

$ curl -f http://localhost:3002/fibonacci/12

{“n”:”12″,”result”:144} 

Over in the window where the service is running, we’ll see a log of GET requests and how long each request took to process:

$ npm run server

> fibonacci@0.0.0 server /Users/David/chap04/fibonacci

> cross-env SERVERPORT=3002 node ./fiboserver

GET /fibonacci/10 200 0.393 ms – 22
GET /fibonacci/11 200 0.647 ms – 22
GET /fibonacci/12 200 0.772 ms – 23
 

That’s easy—using curl, we can make HTTP GET requests. Now, let’s create a simple client program, fiboclient.js, to programmatically call the Fibonacci service:

const http = require(‘http’); [

“/fibonacci/30”, “/fibonacci/20”, “/fibonacci/10”,

“/fibonacci/9”, “/fibonacci/8”, “/fibonacci/7”,

“/fibonacci/6”, “/fibonacci/5”, “/fibonacci/4”,

“/fibonacci/3”, “/fibonacci/2”, “/fibonacci/1”

].forEach((path) => {

console.log(‘${new Date().toISOString()} requesting ${path}’);

var req = http.request({

host: “localhost”,

port: process.env.SERVERPORT, path,

method: ‘GET’

}, res => {

res.on(‘data’, (chunk) => {

console.log(‘${new Date().toISOString()} BODY: ${chunk}’);

});

});

req.end();

});

This is our good friend http.request with a suitable options object. We’re executing it in a loop, so pay attention to the order that the requests are made versus the order the responses arrive.

Then, in package.json, add the following to the scripts section:

“scripts”: {

“start”: “node ./bin/www”,

“server”: “cross-env SERVERPORT=3002 node ./fiboserver” ,

“client”: “cross-env SERVERPORT=3002 node ./fiboclient”

}

Then, run the client app:

$ npm run client

> fibonacci@0.0.0 client /Volumes/Extra/nodejs/Node.js-14-Web- Development/Chapter04/fibonacci

> cross-env SERVERPORT=3002 node ./fiboclient

2020-01-06T03:18:19.048Z requesting /fibonacci/30

2020-01-06T03:18:19.076Z requesting /fibonacci/20

2020-01-06T03:18:19.077Z requesting /fibonacci/10

2020-01-06T03:18:19.077Z requesting /fibonacci/9

2020-01-06T03:18:19.078Z requesting /fibonacci/8

2020-01-06T03:18:19.079Z requesting /fibonacci/7

2020-01-06T03:18:19.079Z requesting /fibonacci/6

2020-01-06T03:18:19.079Z requesting /fibonacci/5

2020-01-06T03:18:19.080Z requesting /fibonacci/4

2020-01-06T03:18:19.080Z requesting /fibonacci/3

2020-01-06T03:18:19.080Z requesting /fibonacci/2

2020-01-06T03:18:19.081Z requesting /fibonacci/1

2020-01-06T03:18:19.150Z BODY: {“n”:”10″,”result”:55}

2020-01-06T03:18:19.168Z BODY: {“n”:”4″,”result”:3}

2020-01-06T03:18:19.170Z BODY: {“n”:”5″,”result”:5}

2020-01-06T03:18:19.179Z BODY: {“n”:”3″,”result”:2}

2020-01-06T03:18:19.182Z BODY: {“n”:”6″,”result”:8}

2020-01-06T03:18:19.185Z BODY: {“n”:”1″,”result”:1}

2020-01-06T03:18:19.191Z BODY: {“n”:”2″,”result”:1}

2020-01-06T03:18:19.205Z BODY: {“n”:”7″,”result”:13}

2020-01-06T03:18:19.216Z BODY: {“n”:”8″,”result”:21}

2020-01-06T03:18:19.232Z BODY: {“n”:”9″,”result”:34}

2020-01-06T03:18:19.345Z BODY: {“n”:”20″,”result”:6765}

2020-01-06T03:18:24.682Z BODY: {“n”:”30″,”result”:832040}

We’re building our way toward adding the REST service to the web application. At this point, we’ve proved several things, one of which is the ability to call a REST service in our program.

We also inadvertently demonstrated an issue with long-running calculations. You’ll notice that the requests were made from the largest to the smallest, but the results appeared in a very different order. Why? This is because of the processing time required for each request, and the inefficient algorithm we’re using. The computation time increases enough to ensure that larger request values have enough processing time to reverse the order.

What happens is that fiboclient.js sends all of its requests right away, and then each one waits for the response to arrive. Because the server is using fibonacciAsync, it will work on calculating all the responses simultaneously. The values that are quickest to calculate are the ones that will be ready first. As the responses arrive in the client, the matching response handler fires, and in this case, the result prints to the console. The results will arrive when they’re ready, and not a millisecond sooner.

We now have enough on our hands to offload Fibonacci calculation to a backend service.

2. Refactoring the Fibonacci application to call the REST service

Now that we’ve implemented a REST-based server, we can return to the Fibonacci application, applying what we’ve learned to improve it. We will lift some of the code from fiboclient.js and transplant it into the application to do this. Create a new file, routes/fibonacci-rest.js, with the following code:

const express = require(‘express’);

const router = express.Router();

const http = require(‘http’);

const math = require(‘../math’);

router.get(‘/’, function (req, res, next) {

if (req.query.fibonum) {

var httpreq = http.request({

host: “localhost”,

port: process.env.SERVERPORT,

path: ‘/fibonacci/${Math.floor(req.query.fibonum)}’,

method: ‘GET’

});

httpreq.on(‘response’, (response) => {

response.on(‘data’, (chunk) => {

var data = JSON.parse(chunk); res.render(‘fibonacci’, {

title: “Calculate Fibonacci numbers”,

fibonum: req.query.fibonum,

fiboval: data.result

});

});

response.on(‘error’, (err) => { next(err); });

});

httpreq.on(‘error’, (err) => { next(err); });

httpreq.end();

} else {

res.render(‘fibonacci’, {

title: “Calculate Fibonacci numbers”,

fiboval: undefined

});

}

});

module.exports = router;

This is a new variant of the Fibonacci route handler, this time calling the REST backend service. We’ve transplanted the http.request call from fiboclient.js and integrated the events coming from the client object with the Express route handler. In the normal path of execution, the HTTPClient issues a response event, containing a response object. When that object issues a data event, we have our result. The result is JSON text, which we can parse and then return to the browser as the response to its request.

In app.js, make the following change:

const index = require(‘./routes/index’);

// const fibonacci = require(‘./routes/fibonacci’);

// const fibonacci = require(‘./routes/fibonacci-async1’);

// const fibonacci = require(‘./routes/fibonacci-await’);

const fibonacci = require(‘./routes/fibonacci-rest’);

This, of course, reconfigures it to use the new route handler. Then, in package.json, change the scripts entry to the following:

“scripts”: {

“start”: “cross-env DEBUG=fibonacci:* node ./bin/www”,

“startrest”: “cross-env DEBUG=fibonacci:* SERVERPORT=3002 node

./fiboserver”,

“server”: “cross-env DEBUG=fibonacci:* SERVERPORT=3002 node

./bin/www”,

“client”: “cross-env DEBUG=fibonacci:* SERVERPORT=3002 node

./fiboclient”

},

How can we have the same value for SERVERPORT for all three scripts entries? The answer is that the variable is used differently in different places. In startrest, this variable is used in routes/fibonacci-rest.js to know at which port the REST service is running. Likewise, in client, fiboclient.js uses this variable for the same purpose. Finally, in server, the fiboserver.js script uses the SERVERPORT variable to know which port to listen on.

In start and startrest, no value is given for PORT. In both cases, bin/www defaults to PORT=3000 if a value is not specified.

In a command window, start the backend server, and in another, start the application. Open a browser window, as before, and make a few requests. You should see an output similar to the following:

$ npm run startrest 

> fibonacci@0.0.0 startrest /Users/David/chap04/fibonacci

> cross-env DEBUG=fibonacci:* SERVERPORT=3002 node ./fiboserver 

GET /fibonacci/34 200 21124.036 ms – 27

GET /fibonacci/12 200 1.578 ms – 23

GET /fibonacci/16 200 6.600 ms – 23

GET /fibonacci/20 200 33.980 ms – 24

GET /fibonacci/28 200 1257.514 ms – 26

 

The output looks like this for the application:

$ npm run server

> fibonacci@0.0.0 server /Users/David/chap04/fibonacci

> cross-env DEBUG=fibonacci:* SERVERPORT=3002 node ./bin/www 

fibonacci:server Listening on port 3000 +0ms

GET /fibonacci?fibonum=34 200 21317.792 ms – 548

GET /stylesheets/style.css 304 20.952 ms – –

GET /fibonacci?fibonum=12 304 109.516 ms – –

GET /stylesheets/style.css 304 0.465 ms – –

GET /fibonacci?fibonum=16 200 83.067 ms – 544

GET /stylesheets/style.css 304 0.900 ms – –

GET /fibonacci?fibonum=20 200 221.842 ms – 545

GET /stylesheets/style.css 304 0.778 ms – –

GET /fibonacci?fibonum=28 200 1428.292 ms – 547

GET /stylesheets/style.css 304 19.083 ms – – 

Because we haven’t changed the templates, the screen will look exactly as it did earlier.

We may run into another problem with this solution. The asynchronous implementation of our inefficient Fibonacci algorithm may cause the Fibonacci service process to run out of memory. In the Node.js FAQs, https://github.com/nodejs/node/wiki/FAQ, it’s suggested to use the — max_old_space_size flag. You’d add this to package.json, as follows:

“server”: “cross-env SERVERPORT=3002 node ./fiboserver — max_old_space_size 5000”,

However, the FAQs also say that if you’re running into maximum memory space problems, your application should probably be refactored. This goes back to the point we made earlier that there are several approaches to addressing performance problems, one of which is the algorithmic refactoring of your application.

Why go through the trouble of developing this REST server when we could just directly use fibonacciAsync?

The main advantage is the ability to push the CPU load for this heavyweight calculation to a separate server. Doing so preserves the CPU capacity on the frontend server so that it can attend to the web browsers. GPU coprocessors are now widely used for numerical computing and can be accessed via a simple network API. The heavy computation can be kept separate, and you can even deploy a cluster of backend servers sitting behind a load balancer, evenly distributing requests.

Decisions such as this are made all the time to create multi-tier systems.

What we’ve demonstrated is that it’s possible to implement simple multi-tier REST services in a few lines of Node.js and Express. This whole exercise gave us a chance to think about computationally intensive code in Node.js and the value of splitting a larger service into multiple services.

Of course, Express isn’t the only framework that can help us create REST services.

3. Some RESTful modules and frameworks

Here are a few available packages and frameworks to assist your REST-based projects:

  • Restify (>http://restify.com/): This offers both client-side and server- side frameworks for both ends of REST transactions. The server-side API is similar to Express.
  • Loopback (http://loopback.io/): This is an offering from StrongLoop. It offers a lot of features and is, of course, built on top of Express.

In this section, we’ve done a big thing in creating a backend REST service.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *