Node.js: Understanding HTTP server applications

The HTTPServer object is the foundation of all Node.js web applications. The object itself is very close to the HTTP protocol, and its use requires knowledge of this protocol. Fortunately, in most cases, you’ll be able to use an application framework, such as Express, to hide the HTTP protocol details. As application developers, we want to focus on business logic.

We already saw a simple HTTP server application in Chapter 2, Setting Up Node.js. Because HTTPServer is an EventEmitter object, the example can be written in another way to make this fact explicit by separately adding the event listener:

import * as http from ‘http’;

const server = http.createServer(); server.on(‘request’, (req, res) => {

res.writeHead(200, {‘Content-Type’: ‘text/plain’});

res.end(‘Hello, World!\n’);

});

server.listen(8124, ‘127.0.0.1’);

console.log(‘Server running at http://127.0.0.1:8124’);

Here, we created an HTTP server object, then attached a listener to the request event, and then told the server to listen to connections from localhost (127.0.0.1) on port 8124. The listen function causes the server to start listening and arranges to dispatch an event for every request arriving from a web browser.

The request event is fired any time an HTTP request arrives on the server. It takes a function that receives the request and response objects. The request object has data from the web browser, while the response object is used to gather data to be sent in the response.

Now, let’s look at a server application that performs different actions based on the URL.

Create a new file named server.mjs, containing the following code:

import * as http from ‘http’;

import * as util from ‘util’;

import * as os from ‘os’;

const listenOn = ‘http://localhost:8124’;

const server = http.createServer();

server.on(‘request’, (req, res) => {

var requrl = new URL(req.url, listenOn);

if (requrl.pathname === ‘/’) homePage(req, res);

else if (requrl.pathname === “/osinfo”) osInfo(req, res); else {

res.writeHead(404, {‘Content-Type’: ‘text/plain’});

res.end(“bad URL “+ req.url);

}

});

server.listen(new URL(listenOn).port);

console.log(‘listening to ${listenOn}’);

function homePage(req, res) {

res.writeHead(200, {‘Content-Type’: ‘text/html’});

res.end(

`<html><head><title>Hello, world!</title></head>

<body><h1>Hello, world!</h1>

<p><a href=’/osinfo’>OS Info</a></p>

</body></html>`);

}

function osInfo(req, res) {

res.writeHead(200, {‘Content-Type’: ‘text/html’}); res.end(

`<html><head><title>Operating System Info</title></head>

<body><h1>Operating System Info</h1>

<table>

<tr><th>TMP Dir</th><td>${os.tmpdir()}</td></tr>

<tr><th>Host Name</th><td>${os.hostname()}</td></tr>

<tr><th>OS Type</th><td>${os.type()} ${os.platform()}

${os.arch()} ${os.release()}</td></tr>

<tr><th>Uptime</th><td>${os.uptime()}

${util.inspect(os.loadavg())}</td></tr>

<tr><th>Memory</th><td>total: ${os.totalmem()} free:

${os.freemem()}</td></tr>

<tr><th>CPU’s</th><td><pre>${util.inspect(os.cpus())}</pre></td></tr>

<tr><th>Network</th><td><pre>${util.inspect(os.networkInterfaces())}</ pre></td></tr>

</table>

</body></html>`);

}

The request event is emitted by HTTPServer every time a request arrives from a web browser. In this case, we want to respond differently based on the request URL, which arrives as req.url. This value is a string containing the URL from the HTTP request. Since there are many attributes to a URL, we need to parse the URL so that we can correctly match the pathname for one of two paths: / and /osinfo.

Parsing a URL with the URL class requires a base URL, which we’ve supplied in the listenOn variable. Notice how we’re reusing this same variable in a couple of other places, using one string to configure multiple parts of the application.

Depending on the path, either the homePage or osInfo functions are called.

This is called request routing, where we look at attributes of the incoming request, such as the request path, and route the request to handler functions.

In the handler functions, the req and res parameters correspond to the request and response objects. Where req contains data about the incoming request, we send the response using res. The writeHead function sets up the return status (200 means success, while 404 means the page is not found) and the end function sends the response.

If the request URL is not recognized, the server sends back an error page using a 404 result code. The result code informs the browser about the status of the request, where a 200 code means everything is fine and a 404 code means the requested page doesn’t exist. There are, of course, many other HTTP response codes, each with their own meaning.

There are plenty more functions attached to both objects, but that’s enough to get us started.

To run it, type the following command:

$ node server.mjs

listening to http://localhost:8124

 Then, if we paste the URL into a web browser, we see something like this:

This application is meant to be similar to PHP’s sysinfo function.

Node.js’s os module is consulted to provide information about the computer. This example can easily be extended to gather other pieces of data.

A central part of any web application is the method of routing requests to request handlers. The request object has several pieces of data attached to it, two of which are useful for routing requests: the request.url and request.method fields.

In server.mjs, we consult the request.url data to determine which page to show after parsing using the URL object. Our needs are modest in this server, and a simple comparison of the pathname field is enough. Larger applications will use pattern matching to use part of the request URL to select the request handler function and other parts to extract request data out of the URL. We’ll see this in action when we look at Express later in the Getting started with Express section.

Some web applications care about the HTTP verb that is used (GET, DELETE, POST, and so on) and so we must consult the request.method field of the request object. For example, POST is frequently used for any FORM submissions.

That gives us a taste of developing servers with Node.js. Along the way, we breezed past one big ES2015 feature—template strings. The template strings feature simplifies substituting values into strings. Let’s see how that works.

1. ES2015 multiline and template strings

The previous example showed two of the new features introduced with ES2015: multiline and template strings. These features are meant to simplify our lives when creating text strings.

The existing JavaScript string representations use single quotes and double quotes. Template strings are delimited with the backtick character, which is also known as the grave accent:

`template string text`

 Before ES2015, one way to implement a multiline string was to use the following construct:

[“<html><head><title>Hello, world!</title></head>”,

“<body><h1>Hello, world!</h1>”,

“<p><a href=’/osinfo’>OS Info</a></p>”,

“</body></html>”]

.join(‘\n’)

This is an array of strings that uses the join function to smash them together into one string. Yes, this is the code used in the same example in previous versions of this book. This is what we can do with ES2015:

`<html><head><title>Hello, world!</title></head>

<body><h1>Hello, world!</h1>

<p><a href=’/osinfo’>OS Info</a></p>

</body></html>`

This is more succinct and straightforward. The opening quote is on the first line, the closing quote is on the last line, and everything in between is part of our string.

The real purpose of the template strings feature is to support easily substituting values directly into strings. Many other programming languages support this ability, and now JavaScript does, too.

Pre-ES2015, a programmer would have written their code like this:

[ …

“<tr><th>OS Type</th><td>{ostype} {osplat} {osarch}

{osrelease}</td></tr>”

… ].join(‘\n’)

.replace(“{ostype}”, os.type())

.replace(“{osplat}”, os.platform())

.replace(“{osarch}”, os.arch())

.replace(“{osrelease}”, os.release())

Similar to the previous snippet, this relied on the replace function to insert values into the string. Again, this is extracted from the same example that was used in previous versions of this book. With template strings, this can be written as follows:

`…<tr><th>OS Type</th><td>${os.type()} ${os.platform()} ${os.arch()}

${os.release()}</td></tr>…`

Within a template string, the part within the ${ .. } brackets is interpreted as an expression. This can be a simple mathematical expression, a variable reference, or, as in this case, a function call.

Using template strings to insert data carries a security risk. Have you verified that the data is safe? Will it form the basis of a security attack? As always, data coming from an untrusted source, such as user input, must be properly encoded for the target context where the data is being inserted. In the example here, we should have used a function to encode this data as HTML, perhaps. But for this case, the data is in the form of simple strings and numbers and comes from a known, safe data source—the built-in os module—and so we know that this application is safe.

For this and many other reasons, it is often safer to use an external template engine. Applications such as Express make it easy to do so.

We now have a simple HTTP-based web application. To gain more experience with HTTP events, let’s add to one to a module for listening to all HTTP events.

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 *