There are 5 major classes of objects in Node’s http module:
net.Server
, which extends from EventEmitter
WritableStream
interface, which extends from EventEmitter
globalAgent
by default, but -ClientRequest
object.IncomingMessage
WritableStream
interface, which extends EventEmitter
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:
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
.
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.
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);
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:
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.