The most surprising thing about Node.js HTTP clients is that the "best" one often isn’t the one with the most features, but the one that gets out of your way the fastest.

Let’s see undici in action, making a simple GET request and then a POST.

import { request } from 'undici';

async function fetchData() {
  try {
    const response = await request('https://httpbin.org/get');
    const data = await response.body.json();
    console.log('GET Response:', data);
  } catch (error) {
    console.error('GET Error:', error);
  }
}

async function postData() {
  try {
    const response = await request('https://httpbin.org/post', {
      method: 'POST',
      body: JSON.stringify({ message: 'hello from undici' }),
      headers: {
        'content-type': 'application/json'
      }
    });
    const data = await response.body.json();
    console.log('POST Response:', data);
  } catch (error) {
    console.error('POST Error:', error);
  }
}

fetchData();
postData();

This code demonstrates basic GET and POST requests. undici uses the native fetch API, which is becoming the standard in modern JavaScript environments. The request function returns a promise that resolves with a Response object. You then access the body, which can be parsed as JSON, text, or consumed as a stream.

Now, let’s look at got. It’s known for its powerful features and flexibility.

import got from 'got';

async function fetchDataGot() {
  try {
    const response = await got('https://httpbin.org/get', {
      method: 'GET',
      responseType: 'json'
    });
    console.log('GET Response (got):', response.body);
  } catch (error) {
    console.error('GET Error (got):', error);
  }
}

async function postDataGot() {
  try {
    const response = await got.post('https://httpbin.org/post', {
      json: { message: 'hello from got' },
      responseType: 'json'
    });
    console.log('POST Response (got):', response.body);
  } catch (error) {
    console.error('POST Error (got):', error);
  }
}

fetchDataGot();
postDataGot();

Notice got’s more declarative API. For POST requests, you can directly pass a json object, and got handles the JSON.stringify and setting the content-type header for you. The responseType: 'json' option automatically parses the response body.

Finally, axios, the long-standing champion of many Node.js projects.

import axios from 'axios';

async function fetchDataAxios() {
  try {
    const response = await axios.get('https://httpbin.org/get');
    console.log('GET Response (axios):', response.data);
  } catch (error) {
    console.error('GET Error (axios):', error);
  }
}

async function postDataAxios() {
  try {
    const response = await axios.post('https://httpbin.org/post', {
      message: 'hello from axios'
    });
    console.log('POST Response (axios):', response.data);
  } catch (error) {
    console.error('POST Error (axios):', error);
  }
}

fetchDataAxios();
postDataAxios();

axios also offers a very straightforward API. The response data is directly available in response.data, abstracting away the JSON parsing.

Under the hood, all these libraries are managing the low-level details of creating TCP connections, sending HTTP requests, handling headers, and parsing responses. undici is built on Node.js’s native fetch implementation, making it highly performant and aligned with web standards. got is a mature library with a rich feature set, including advanced features like automatic retries, request cancellation, and sophisticated error handling. axios is known for its ease of use, broad compatibility (works in browsers and Node.js), and robust interceptor system for request and response manipulation.

The core problem they all solve is making it trivial to interact with remote services over HTTP, abstracting away the complexities of network programming. You provide a URL, a method, and potentially some data, and the library handles the rest. They manage connection pooling, keep-alive, TLS/SSL, and status code checking, so you don’t have to.

The one thing most people don’t realize is how much configuration is available for optimizing performance, especially with got and axios. For instance, setting keepAlive: true on a got client instance, or using http.Agent with axios, can drastically reduce latency for repeated requests to the same host by reusing underlying TCP connections instead of establishing a new one each time. This is a fundamental optimization for any application making many HTTP calls.

The next logical step is to explore how these libraries handle more complex scenarios like streaming uploads/downloads, authentication, and advanced error handling strategies.

Want structured learning?

Take the full Nodejs course →