📣 This post originally appeared as Use the Node.js HTTP Module to Make a Request on The Bearer Blog.
The ecosystem around making requests in Node.js applications is huge. With countless libraries available, it can be helpful to understand why they exist. This helps to improve your understanding of Node.js as a whole, and makes choosing an HTTP client easier.
In the first post in this series, we looked at creating servers using Node's http
module. In this post, we will explore making a request using the http.request
method and handling the response. This is the underlying component used by most, if not all, Node.js request libraries.
http.request basics
The request
method is part of Node's built-in http
module. This module handles much of the low-level functionality needed to create servers, receive requests, send responses, and keep connections open. The request
implementation, like most core implementations, is rather verbose and harder to work with than the abstractions that many libraries implement. It is also event-driven, and relies on streams to handle data. This doesn't mean it isn't usable. In fact, many parts of it will look familiar to third-party libraries as they have drawn inspiration from it. Let's start with a basic request.
const http = require("http")
http
.request(
{
hostname: "postman-echo.com",
path: "/status/200"
},
res => {
let data = ""
res.on("data", d => {
data += d
})
res.on("end", () => {
console.log(data)
})
}
)
.end()
This code block makes a GET
request to http://postman-echo.com/status/200
and logs the response to the console. The request
method can take a variety of configuration options. In this example, we are passing it the hostname and path. We didn't set a method, because GET
is the default. The callback takes the response—res
in the example—which can listen for events that fire during the response.
This example focuses on two key events. The data
event and the end
event. Because the response comes as a readable stream, we need to assemble it. For stringified responses you can build a string. Alternately, it may be a good idea to push to an array and then use a buffer to assemble the result like we do in the createServer article.
Each time the data
event fires we append to a string. Finally, when the end
event fires we log the result. At the end of the call, we chain the end()
method. This is a required part of the request, and without it the API call will not fire.
Let's look at another implementation of the same code.
const http = require("http")
let options = new URL("https://postman-echo.com/status/200")
let myRequest = http.request(options, res => {
// Same as previos example
res.on('data' d=> {
//...
})
//... etc
})
myRequest.on("error", console.error)
myRequest.end()
In this version of the code, we create a URL
with our desired API endpoint. The request
can now take this URL object as the first argument. We also assign the whole code block to myRequest
. This gives myRequest
the ability to control the request listeners rather than chaining them to the end.
You may be tempted to try and call myRequest()
, but the action that fires the request is .end()
.
Shorthand requests with http.get
While http.request
can handle all the major HTTP methods, GET
has a dedicated shorthand method. It works exactly the same, except it accepts a url string, removes the need for .end()
, and sets the method to GET
. For example, our first example would look as follows with http.get
:
const http = require("http")
http.get("https://postman-echo.com/status/200", res => {
let data = ""
res.on("data", d => {
data += d
})
res.on("end", () => {
console.log(data)
})
})
While not a major difference, this syntax makes handling GET
requests easier.
POSTing to an API
With common GET
request handled, let's look at POST
. The syntax is mostly the same.
const http = require("http")
let body = JSON.stringify({
title: "Make a request with Node's http module"
})
let options = {
hostname: "postman-echo.com",
path: "/post",
method: "POST",
headers: {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(body)
}
}
http
.request(options, res => {
let data = ""
res.on("data", d => {
data += d
})
res.on("end", () => {
console.log(data)
})
})
.on("error", console.error)
.end(body)
First we need to set up the body and options. Since we want to send JSON, we stringify the object and set it to body
. Next, configure options
with the necessary properties and headers. Note that we are telling the server the size of our payload with the Content-Length
header, as well as the type of data with Content-Type
.
The http.request
portion looks mostly the same as in the earlier examples. We chain an on("error")
listener. We also pass the body into the end()
. This could also be written as .write(body).end()
.
Things to watch out for
While the code examples above are average use-cases, there are a few quirks to be aware of. Here are a few best practices and tips for working with http.request
.
HTTPS
In the examples we use http
, but for secure connections requiring HTTPS, you can use the https
module in the same way. It is compatible with all the functionality of http
.
Watch for empty responses
Empty responses won't fire a data
event. This means if your response.on('end')
event expects something from the data listener, you may run into problems for some responses like those from redirects. Make sure to perform any checks before relying on data coming from the data
listener.
Make requests from inside the server
The host
property in the options object defaults to localhost
. This is nice for quick local experiments and instances where you want to call a server from within itself. For example:
let server = http.createServer()
server.listen(3000, error => {
http.request({
port: 3000,
path: "/endpoint"
}, res => {
// handle the response
})
})
Using this technique, you can make calls upon the server from inside the listen
method's callback.
Error handling
One of the earlier examples briefly shows error handling, but it is worth mentioning again. The request itself, not the response, can fire an error
event. You can listen for it by chaining .on('error', (error) => {})
onto a request before calling .end()
or if you have set the request to a variable, you can listen on the variable.
let call = http.request(options, handleResponse)
call.on("error", handleError)
call.end()
This is also a good use case for building a custom error type to handle specific responses.
Cancelling requests
The req.abort()
method allows you to cancel requests if used before the connection completes. In our example where the request is named call
, this would be call.abort()
.
Wrapping up
So should you use http.request
on it's own? In most cases it is a better choice to use one of the many libraries available on NPM. Especially one that supports promises and async/await. That said, know that beneath them all is the http
module. You can take advantage of its features to built functionality on top of the requests that your application makes.
At Bearer, we use the underlying functionality of http.request
to actively monitor and control calls made to third-party APIs and web services. Explore the rest of the Bearer Blog for more on Node.js and interacting with APIs.