- Published on
Client-Server Communication with Node.js
- Authors
- Name
- Dan Orlando
- @danorlando1
There are 5 major classes of objects in Node's http module:
- http.Server
- Used to create the basic server
- Inherits from
net.Server
, which extends fromEventEmitter
- http.ServerResponse
- Gets created internally by an http server
- Implements the
WritableStream
interface, which extends fromEventEmitter
- 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.
- 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 extendsEventEmitter
- When we initiate an HTTP request, we are working with a
- http.IncomingMessage
- This is the server request
- Implements the
ReadableStream
interface, which extends fromEventEmitter
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();
IncomingMessage
object: 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
.
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.