Making HTTP Requests from Node.js

Difficulty: Beginner

Node.js is not just for serving HTTP responses - it can also make outgoing HTTP requests to other servers. This is essential for consuming third-party APIs, communicating between microservices, fetching data from external sources, and building proxy servers. Node.js provides multiple ways to make HTTP requests, from built-in modules to the modern fetch API.

The built-in 'http' and 'https' modules provide http.get() and http.request() functions for making outgoing requests. These are callback-based and stream-oriented: the response arrives as a stream of data chunks that you must collect and concatenate. While this approach is efficient for large responses, it requires more boilerplate code than most developers would like. The http.get() function is a convenience method for GET requests that automatically calls req.end() for you.

Starting with Node.js 18, the global fetch() function is available without any imports, matching the same API that browsers have had for years. This is now the recommended way to make HTTP requests in Node.js for most use cases. fetch() returns a Promise that resolves with a Response object, and you can call response.json() to parse a JSON body or response.text() for plain text. It is much cleaner than the callback-based approach.

When consuming external APIs, proper error handling is critical. Network requests can fail for many reasons: the server might be down, the network might be unreliable, the API might return an error status code, or the response might not be valid JSON. You should always handle both network-level errors (the request itself failed) and application-level errors (the server returned a 4xx or 5xx status code). Note that fetch() does NOT throw on HTTP error status codes - a 404 response is still a successful fetch; you must check response.ok or response.status yourself.

For production applications, you will often use third-party libraries like axios or node-fetch (for older Node.js versions) which provide a more ergonomic API with features like automatic JSON parsing, request/response interceptors, timeout handling, and request cancellation. However, with fetch() now built into Node.js, the need for these libraries has decreased significantly.

Code examples

Making Requests with Built-in http/https

const https = require('https');

// Simple GET request
https.get('https://jsonplaceholder.typicode.com/users/1', (res) => {
  let data = '';

  // Collect data chunks
  res.on('data', (chunk) => {
    data += chunk;
  });

  // Parse the complete response
  res.on('end', () => {
    const user = JSON.parse(data);
    console.log('User:', user.name);
    console.log('Email:', user.email);
  });
}).on('error', (err) => {
  console.error('Request failed:', err.message);
});

https.get() makes a GET request and returns the response as a stream. You must collect all chunks in the 'data' event and process the complete response in the 'end' event. The 'error' event handles network-level failures.

Using fetch() (Node.js 18+)

// GET request with fetch
async function getUser() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/users/1');

    if (!response.ok) {
      throw new Error(`HTTP error! Status: ${response.status}`);
    }

    const user = await response.json();
    console.log('User:', user.name);
    console.log('Company:', user.company.name);
  } catch (error) {
    console.error('Failed to fetch user:', error.message);
  }
}

// POST request with fetch
async function createPost() {
  try {
    const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        title: 'My New Post',
        body: 'This is the content of my post.',
        userId: 1
      })
    });

    const newPost = await response.json();
    console.log('Created post with ID:', newPost.id);
  } catch (error) {
    console.error('Failed to create post:', error.message);
  }
}

getUser();
createPost();

fetch() provides a Promise-based API that works with async/await. For POST requests, you pass an options object with method, headers, and body. Always check response.ok because fetch() does not throw on HTTP error codes like 404 or 500.

Building an API Proxy Server

const http = require('http');

// Server that proxies requests to an external API
const server = http.createServer(async (req, res) => {
  const sendJSON = (code, data) => {
    res.writeHead(code, {
      'Content-Type': 'application/json',
      'Access-Control-Allow-Origin': '*'
    });
    res.end(JSON.stringify(data));
  };

  if (req.method === 'GET' && req.url === '/api/weather') {
    try {
      // Fetch data from external API
      const response = await fetch(
        'https://api.open-meteo.com/v1/forecast?latitude=40.71&longitude=-74.01&current_weather=true'
      );
      const weather = await response.json();

      // Transform and send only what the client needs
      sendJSON(200, {
        temperature: weather.current_weather.temperature,
        windSpeed: weather.current_weather.windspeed,
        description: weather.current_weather.weathercode
      });
    } catch (error) {
      sendJSON(502, { error: 'Failed to fetch weather data' });
    }
  } else {
    sendJSON(404, { error: 'Not Found' });
  }
});

server.listen(3000, () => {
  console.log('Proxy server running on port 3000');
});

A proxy server fetches data from external APIs on behalf of the client. This pattern is used to hide API keys, add caching, transform response data, or bypass CORS restrictions. Status 502 (Bad Gateway) indicates the upstream server failed.

Key points

Concepts covered

http.get, https module, fetch API, Response Handling, API Consumption, Promises