Making HTTP requests with Node

By

Learn how to make HTTP requests in Node.js with the built-in fetch() function, plus the lower-level https module, for GET, POST, PUT, and DELETE requests.

~~~

You can make HTTP requests in Node.js with the global fetch() function, no external library needed. It’s the same Fetch API you use in the browser. It’s built into Node since version 18 and stable since Node 21, so it’s there in Node 24, the current LTS.

Node also ships the older https module, which gives you lower-level control. I cover it at the end of this post.

I use the term HTTP, but HTTPS is what should be used everywhere, therefore these examples use HTTPS instead of HTTP.

Perform a GET request with fetch()

Pass the URL to fetch(), wait for the response, then parse the body:

const response = await fetch('https://flaviocopes.com/todos')

if (!response.ok) {
  throw new Error(`Request failed with status ${response.status}`)
}

const todos = await response.json()
console.log(todos)

fetch() returns a promise, and so does response.json(). That’s why we await both.

Check response.ok before reading the body. fetch() only rejects on network errors, like a failed DNS lookup. A 404 or a 500 still resolves, and response.ok is false.

I’m using await at the top level here. That works in an ES module: a .mjs file, or a project with "type": "module" in package.json. In a CommonJS file, put the code inside an async function. My async/await tutorial explains how that works.

Perform a POST request with fetch()

A POST needs three more things: the method, a header that describes the body, and the body itself:

const response = await fetch('https://flaviocopes.com/todos', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    todo: 'Buy the milk'
  })
})

if (!response.ok) {
  throw new Error(`Request failed with status ${response.status}`)
}

const result = await response.json()
console.log(result)

The body must be a string, so we turn the object into JSON with JSON.stringify(). The Content-Type header tells the server it’s JSON.

You don’t set Content-Length yourself. fetch() calculates it from the body.

For PUT and DELETE requests, change method to 'PUT' or 'DELETE'.

Using the https module

Before fetch() arrived, the built-in https module was the way to make requests in Node. It’s lower level. You describe the request in an options object, pass it to https.request(), and read the response as it streams in.

You’ll still find it in a lot of existing code, so it’s good to know how it works.

Perform a GET request with https.request()

The options object holds the pieces of the request: the host, the port (443 for HTTPS), the path, and the HTTP method:

const https = require('node:https')
const options = {
  hostname: 'flaviocopes.com',
  port: 443,
  path: '/todos',
  method: 'GET'
}

const req = https.request(options, (res) => {
  console.log(`statusCode: ${res.statusCode}`)

  res.on('data', (d) => {
    process.stdout.write(d)
  })
})

req.on('error', (error) => {
  console.error(error)
})

req.end()

A few things to notice here.

The callback receives the response object res before the body has arrived. The body comes in as a stream, in chunks, through the data events. That’s why we write each chunk as it arrives instead of logging one big string.

The error handler is not optional in practice. If the DNS lookup fails or the server refuses the connection, and no error listener is attached, Node crashes the whole process.

And don’t forget req.end(). The request is not actually sent until you call it. Forget it and the program hangs, waiting forever.

Perform a POST request with https.request()

A POST works the same way, with two additions: headers describing the body, and the body itself written with req.write():

const https = require('node:https')

const data = JSON.stringify({
  todo: 'Buy the milk'
})

const options = {
  hostname: 'flaviocopes.com',
  port: 443,
  path: '/todos',
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Content-Length': data.length
  }
}

const req = https.request(options, (res) => {
  console.log(`statusCode: ${res.statusCode}`)

  res.on('data', (d) => {
    process.stdout.write(d)
  })
})

req.on('error', (error) => {
  console.error(error)
})

req.write(data)
req.end()

Be careful with Content-Length. Here data.length works because the string is plain ASCII. But .length counts characters, not bytes, and the header needs bytes. Send 'Comprare il caffè' and the two numbers differ, because è takes 2 bytes in UTF-8. The safe version is:

'Content-Length': Buffer.byteLength(data)

Use Buffer.byteLength() any time the body might contain accented letters, emoji, or any non-ASCII text.

PUT and DELETE with https.request()

PUT and DELETE requests use the same POST request format, and just change the options.method value.

If you want to try these requests from the command line first, I made a free route to curl tool that builds the curl command for you.

Tagged: Node.js · All topics

Want me to talk about your product? You can sponsor this site.

~~~

Related posts about node: