Published on

Client-Server Communication with Node.js

Authors

There are 5 major classes of objects in Node's http module:

  1. http.Server
  • Used to create the basic server
  • Inherits from net.Server, which extends from EventEmitter
  1. http.ServerResponse
  • Gets created internally by an http server
  • Implements the WritableStream interface, which extends from EventEmitter
  1. http.Agent
    • Used to manage pooling sockets used in HTTP client requests
    • Node uses a globalAgent by default, but -
    • We can create a different agent with different options when we need to.
  2. http.ClientRequest
    • When we initiate an HTTP request, we are working with a ClientRequest object.
    • this is different from the server request object, which is an IncomingMessage
    • Implements the WritableStream interface, which extends EventEmitter
  3. http.IncomingMessage
    • This is the server request
    • Implements the ReadableStream interface, which extends from EventEmitter

You'll notice that all of these classes except for http.Agent inherit from EventEmitter. This means we can listen for and respond to events on these objects.

Making a request to a server from Node is as simple as calling the http.request method with the hostname and callback function.

const http = require('http');
const req = http.request({hostname: 'http://www.google.com'}, (res) => {
  console.log(res);
});

req.on('error', (e) => console.log(e));
req.end();
This will output the IncomingMessage object: Logging the IncomingMessage

When the server gets a response from the request, the 'data' event is emitted. We can listen for this event and log the response body. This time instead of logging the entire the entire IncomingMessage object, we'll log the statusCode and headers of the response along with the response body. We'll also change http.request to use the http.get method, which takes care of the req.end() call for us.

const http = require('http');
const req = http.get({hostname: 'http://www.google.com'}, (res) => {
  console.log(res.statusCode);
  consoe.log(res.headers);
  res.on('data', (data) => {
    console.log(data.toString());
  });
});

req.on('error', (e) => console.log(e));

This will log the html that comes back from the request as a string to the console along with the status code and headers. Note that https is done the same way - simply require('https') instead of require('http') and call https.get instead of http.get.

Working with Routes

Routes can be handled by listening for the 'request' event and handling it with a switch statement. We'll use the readFileSync method from the fs module to read the file we want to serve.

const fs = require('fs');
const server = http.createServer();
  
server.on('request', (req, res) => {
  switch (req.url) {
    case '/home':
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync('index.html'));
      break;
    case '/about':
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync('about.html'));
      break;
    default:
      res.writeHead(404, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync('404.html'));
  }
});

server.listen(8000);

In this example, when the request event fires, we use the request's url property to determine which file to serve.

We can simplify this by using a template string to resolve the request:

const fs = require('fs');
const server = http.createServer();

server.on('request', (req, res) => {
  switch (req.url) {
    case '/home':
    case '/about':
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync(`${req.url}.html`));
      break;
    default:
      res.writeHead(404, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync('404.html'));
  }
});

server.listen(8000);

When working with routes, we might want to redirect a request to a different page. In this example, we'll redirect the / route to home:

const fs = require('fs');
const server = http.createServer();

server.on('request', (req, res) => {
  switch (req.url) {
    case '/home':
    case '/about':
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync(`${req.url}.html`));
      break;
    case '/':
      res.writeHead(301, {'Location': '/home'});
      res.end();
      break;
    default:
      res.writeHead(404, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync('404.html'));
  }
});

server.listen(8000);

Notice that we're defaulting the location to the 404 page because if the request's url does not match any of the cases in the switch, we can assume that the request is for a page that doesn't exist.

Sending Back JSON Data

Let's say that instead of returning an html page, we want to send back JSON data. We can do this by setting the Content-Type header accordingly, and sending the JSON data in the res.end instead of fs.readFileSync.

const fs = require('fs');
const server = http.createServer();
const data = {
  users: {
    {name: 'John'}, 
    {name: 'Jane'}
  }
};

server.on('request', (req, res) => {
  switch (req.url) {
    case '/api/users':
      res.writeHead(200, {'Content-Type': 'application/json'});
      res.end(JSON.stringify(data));
      break;
    case '/home':
    case '/about':
      res.writeHead(200, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync(`${req.url}.html`));
      break;
    case '/':
      res.writeHead(301, {'Location': '/home'});
      res.end();
      break;
    default:
      res.writeHead(404, {'Content-Type': 'text/html'});
      res.end(fs.readFileSync('404.html'));
  }
});

server.listen(8000);

Parsing URLs and Query Strings

Node has a URL module that can be used for parsing url strings. It includes methods such as parse and format.

The following diagram shows all of the properties of the a URL object: URL object

Passing true as the second argument in url.parse will parse the query string into an object (useful if trying to capture multiple query params)

Reading a specific query param is as simple as: url.parse('https://path/search?q=one, true).query.q This will output one.

The inverse method of parse is format. If you have a situation where you need to format elements into a url, you can use format:

url.format({
  protocol: 'https',
  hostname: 'www.google.com',
  pathname: '/search',
  query: {
    q: 'somequery'
  }
});

This will output: https://www.google.com/search?q=somequery.

When you are only interested in the query params of a url, you can use the querystring module instead, which gives you encode/decode methods along with parse, stringify, and escape/unescape. Parse and stringify are the most important methods here. Parse will give you the query params as an object, and stringify will give you a parsed object as a string.

Node's HTTP module is a crucial part of th Node's client-server communication pattern. To demonstrate this, we looked at the 5 parts of the HTTP module and how to use them to serve files and data.