# The Most Efficient Way to Make an HTTP GET Request in Node.js Within Express

> Boost your Node.js Express app performance by efficiently making HTTP GET requests. Stream responses directly to avoid memory buffering and optimize your code.

- Repository: [expressjs/express](https://github.com/expressjs/express)
- Tags: how-to-guide
- Published: 2026-02-16

---

**The most efficient way to make an HTTP GET request in Node.js within an Express application is to use the native `http` or `https` modules and stream the response directly into the Express `res` object, eliminating memory buffering.**

When an Express server receives a request, it constructs lightweight **`req`** and **`res`** objects based on the prototypes defined in [`lib/request.js`](https://github.com/expressjs/express/blob/main/lib/request.js) and [`lib/response.js`](https://github.com/expressjs/express/blob/main/lib/response.js) within the `expressjs/express` repository. To perform an outbound **HTTP GET request in Node.js** while maintaining maximum throughput, you should leverage Node.js’s native streaming capabilities rather than higher-level libraries that materialize entire responses into memory.

## Why Native Streaming is the Most Efficient Pattern

Streaming the response from an upstream server directly to the client avoids materializing the entire payload in memory. This approach aligns with how Express itself handles data flow, as seen in [`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js) where the framework manages the request-response lifecycle.

**Key efficiency benefits:**

- **Zero intermediate buffering**: By piping the upstream `IncomingMessage` into the Express `ServerResponse`, data transfers chunk-by-chunk without being fully loaded into the Node.js heap.
- **Single event-loop turn**: The `http.request` or `https.request` methods utilize Node’s native event-driven I/O, which is faster than abstraction layers that add promise overhead or additional middleware.
- **Header preservation**: You can forward the remote status code and headers unchanged using `res.writeHead`, maintaining caching semantics and content-type accuracy (referencing header handling patterns in [`lib/response.js`](https://github.com/expressjs/express/blob/main/lib/response.js)).
- **Minimal error latency**: Errors are caught via the `'error'` event on the outbound request, allowing immediate termination with a `502 Bad Gateway` response without waiting for timeouts.

## Implementing Efficient HTTP GET Requests in Express

### The Streaming Proxy Pattern (Zero Buffering)

The most performant implementation pipes the upstream response directly into the Express response object. This pattern is ideal for proxying external APIs where you want to minimize memory footprint.

```javascript
const express = require('express');
const https = require('node:https');
// const http = require('node:http'); // Use for HTTP endpoints

const app = express();

/**
 * GET /proxy?url=https://api.example.com/data
 * Streams the remote GET response directly to the client.
 */
app.get('/proxy', (req, res) => {
  const target = new URL(req.query.url);
  const client = target.protocol === 'https:' ? https : require('node:http');

  const options = {
    hostname: target.hostname,
    path: target.pathname + target.search,
    method: 'GET',
    headers: {
      accept: req.headers.accept || '*/*',
    },
  };

  const upstream = client.request(options, (upstreamRes) => {
    // Forward status code and headers exactly as received
    res.writeHead(upstreamRes.statusCode, upstreamRes.headers);
    // Stream data without buffering
    upstreamRes.pipe(res);
  });

  upstream.on('error', (err) => {
    console.error('Upstream error:', err);
    if (!res.headersSent) {
      res.status(502).send('Bad Gateway');
    } else {
      res.end();
    }
  });

  upstream.end();
});

app.listen(3000, () => console.log('Server listening on http://localhost:3000'));

```

**Key implementation details:**

- `upstreamRes.pipe(res)` establishes a direct data flow from the remote server to the client.
- `res.writeHead` mirrors the original HTTP status and headers, ensuring content-type and cache-control directives remain intact.
- Error handling checks `res.headersSent` to avoid crashing if the response has already begun streaming.

### Async/Await Wrapper with Streaming Preservation

For developers who prefer modern asynchronous patterns, you can wrap the native request in a Promise while still maintaining the streaming efficiency. This approach is useful when you need to perform validation or logging before initiating the stream.

```javascript
const express = require('express');
const { request } = require('node:https');

const app = express();

/**
 * Helper that returns a Promise resolving to the response stream.
 * Does not buffer the body.
 */
function getStream(url) {
  const target = new URL(url);
  const client = target.protocol === 'https:' ? require('node:https') : require('node:http');

  return new Promise((resolve, reject) => {
    const req = client.request(
      {
        hostname: target.hostname,
        path: target.pathname + target.search,
        method: 'GET',
        headers: { accept: 'application/json' },
      },
      (res) => resolve(res)
    );

    req.on('error', reject);
    req.end();
  });
}

app.get('/async-proxy', async (req, res) => {
  try {
    const upstream = await getStream(req.query.url);
    res.writeHead(upstream.statusCode, upstream.headers);
    upstream.pipe(res);
  } catch (err) {
    console.error(err);
    res.status(502).send('Bad Gateway');
  }
});

app.listen(3000);

```

Even though the code uses `async/await` for readability, the actual data transfer occurs via `upstream.pipe(res)`, ensuring that memory usage remains constant regardless of response size.

## Key Express Source Files Supporting This Pattern

Understanding how Express constructs the request and response objects helps explain why native streaming integrates seamlessly:

| File | Purpose | Location |
|------|---------|----------|
| [`lib/express.js`](https://github.com/expressjs/express/blob/main/lib/express.js) | Exposes `createApplication()` which initializes the Express `app` function and merges the request/response prototypes | [view](https://github.com/expressjs/express/blob/master/lib/express.js) |
| [`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js) | Implements the `app` prototype, including route registration (`app.get`, `app.post`) and middleware stack handling (lines 71‑80) | [view](https://github.com/expressjs/express/blob/master/lib/application.js) |
| [`lib/request.js`](https://github.com/expressjs/express/blob/main/lib/request.js) | Defines the **`req`** prototype that Express augments for each incoming request | [view](https://github.com/expressjs/express/blob/master/lib/request.js) |
| [`lib/response.js`](https://github.com/expressjs/express/blob/main/lib/response.js) | Defines the **`res`** prototype, including `res.send()`, `res.json()`, and `res.writeHead()` used to send data back to the client | [view](https://github.com/expressjs/express/blob/master/lib/response.js) |

These files demonstrate that Express is a thin layer over Node.js’s native `http` module. By using `http.request` or `https.request` directly within your route handlers, you operate at the same abstraction level as the framework itself, minimizing overhead.

## Summary

- **Stream directly**: Use `http.request` or `https.request` and pipe the `IncomingMessage` into the Express `res` object to avoid memory buffering.
- **Preserve headers**: Forward status codes and headers using `res.writeHead` to maintain caching and content-type semantics.
- **Handle errors immediately**: Listen for the `'error'` event on the outbound request and return `502 Bad Gateway` if the upstream fails.
- **Leverage native modules**: The `expressjs/express` repository builds on Node’s native `http` module (see [`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js) and [`lib/response.js`](https://github.com/expressjs/express/blob/main/lib/response.js)), making native streaming the most cohesive and performant choice for HTTP GET requests in Node.js.

## Frequently Asked Questions

### Is using native http/https modules faster than axios or node-fetch in Express?

Yes. Libraries like **axios** and **node-fetch** typically buffer the entire response body into memory before resolving, or they wrap streams in additional Promise machinery that adds CPU overhead. Using Node’s native `http.request` or `https.request` allows you to pipe data directly from the socket to the Express `res` object, keeping memory usage constant and reducing latency.

### How do I handle errors when streaming HTTP GET requests in Node.js?

Attach an `'error'` event listener to the outbound request object returned by `http.request`. If an error occurs before headers are sent to the client, respond with a `502 Bad Gateway` status. If headers have already been sent (the stream has started), immediately call `res.end()` to terminate the connection. Always check `res.headersSent` to avoid attempting to send headers twice, which would crash the process.

### Can I modify the response body when streaming an HTTP GET request?

Modifying the body during a pure stream pipe requires inserting a `Transform` stream between the upstream response and the Express `res` object. You can use Node’s built-in `stream.Transform` to manipulate chunks as they pass through. Be aware that any transformation introduces memory overhead proportional to the chunk size being processed, breaking the zero-buffering optimization of a direct pipe.

### What is the difference between req and res objects in Express?

The **`req`** (request) object represents the incoming HTTP request and contains properties like `req.query`, `req.params`, and `req.headers`. It is built from the prototype defined in [`lib/request.js`](https://github.com/expressjs/express/blob/main/lib/request.js). The **`res`** (response) object represents the outgoing HTTP response and provides methods like `res.send()`, `res.json()`, and `res.writeHead()`. It is built from the prototype defined in [`lib/response.js`](https://github.com/expressjs/express/blob/main/lib/response.js). Together, they form the interface for handling HTTP traffic within the Express framework, as orchestrated by [`lib/application.js`](https://github.com/expressjs/express/blob/main/lib/application.js).