Click here to share your insights & best practices about third-party APIs

Use the Node.js HTTP Module to Make a Request

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 manage, monitor, and control calls made to third-party APIs, API integrations, and web services. Explore the rest of the Bearer Blog for more on Node.js and interacting with APIs.

You may also like

Consume APIs. Stay in Control.

Monitor, track performance, detect anomalies, and fix issues on your critical API usage.

Learn more Schedule a Demo