Making HTTP Requests - Client Side
Master Node.js HTTP client - http.request, http.get, fetch, and advanced patterns like retries, timeouts, and streaming
Client-Side HTTP in Node.js
Node.js provides several ways to make outgoing HTTP requests:
| API | Built-in? | Promises? | Stream? |
|---|---|---|---|
http.get() |
Yes (core) | No (callback) | Yes |
http.request() |
Yes (core) | No (callback) | Yes |
fetch() |
Yes (Node 18+, global) | Yes | Yes |
undici |
Yes (core, Node 20+) | Yes | Yes |
1. http.get - Simple GET Requests
The simplest way to fetch data:
const http = require('http');
http.get('http://api.example.com/users', (res) => {
let data = '';
// Collect response chunks
res.on('data', (chunk) => {
data += chunk;
});
// Response complete
res.on('end', () => {
console.log('Status:', res.statusCode);
console.log('Headers:', res.headers);
try {
const users = JSON.parse(data);
console.log('Users:', users);
} catch (err) {
console.error('Parse error:', err.message);
}
});
}).on('error', (err) => {
console.error('Request failed:', err.message);
});
http.get Key Points
http.get()callsreq.end()automatically - no need to call it manually- The callback receives an
http.IncomingMessage(same type as serverreq) - The response is a Readable stream - listen for
'data'and'end' - Always handle
'error'events on the request object
HTTP Status Code Handling
function fetchJSON(url) {
return new Promise((resolve, reject) => {
http.get(url, (res) => {
const { statusCode, headers } = res;
// Handle redirects
if (statusCode >= 300 && statusCode < 400 && headers.location) {
return resolve(fetchJSON(headers.location));
}
// Handle errors
if (statusCode < 200 || statusCode >= 300) {
reject(new Error(`HTTP ${statusCode}: ${statusMessage}`));
res.resume(); // Consume response to free memory
return;
}
// Collect body
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => {
try {
resolve(JSON.parse(Buffer.concat(chunks).toString()));
} catch (err) {
reject(new Error('Invalid JSON response'));
}
});
}).on('error', reject);
});
}
// Usage
try {
const data = await fetchJSON('https://api.github.com/users/octocat');
console.log('User:', data.login, '-', data.name);
} catch (err) {
console.error('Failed:', err.message);
}
2. http.request - Full Control
http.request gives you complete control over the request - method, headers, body, and more:
const http = require('http');
function makeRequest(options, body = null) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
const chunks = [];
res.on('data', (chunk) => chunks.push(chunk));
res.on('end', () => {
const data = Buffer.concat(chunks);
resolve({
statusCode: res.statusCode,
statusMessage: res.statusMessage,
headers: res.headers,
body: data.toString(),
json: () => JSON.parse(data.toString()),
});
});
});
req.on('error', reject);
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Request timed out'));
});
if (body) {
req.write(body);
}
req.end(); // Must call end() to send the request
});
}
GET Request
const response = await makeRequest({
hostname: 'api.example.com',
port: 443,
path: '/users?page=1',
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': 'Node.js Client/1.0',
},
});
console.log('Status:', response.statusCode);
console.log('Body:', response.body);
POST Request
const postData = JSON.stringify({
name: 'Alice',
email: '[email protected]',
});
const response = await makeRequest({
hostname: 'api.example.com',
port: 443,
path: '/users',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'Authorization': 'Bearer my-token',
},
}, postData);
if (response.statusCode === 201) {
const user = response.json();
console.log('Created user:', user);
}
PUT Request
const updateData = JSON.stringify({ name: 'Updated Name' });
const response = await makeRequest({
hostname: 'api.example.com',
path: '/users/42',
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(updateData),
},
}, updateData);
DELETE Request
const response = await makeRequest({
hostname: 'api.example.com',
path: '/users/42',
method: 'DELETE',
});
3. http.request Options Object
The first argument to http.request can be:
a) URL string or URL object
http.request('http://api.example.com/users', callback);
http.request(new URL('http://api.example.com/users'), callback);
b) Options object
const options = {
// URL components
hostname: 'api.example.com', // Host without port
port: 443, // Default: 80 for http, 443 for https
path: '/users?page=1', // Path + query string
method: 'GET', // Default: GET
// Authentication
auth: 'username:password', // Basic auth (sent as header)
// Headers
headers: {
'Content-Type': 'application/json',
'User-Agent': 'MyApp/1.0',
},
// Timeouts
timeout: 5000, // Socket timeout in ms
// Connection management
keepAlive: true, // Reuse connections (HTTP/1.1)
keepAliveMsecs: 1000, // Initial keep-alive delay
// Socket options
family: 4, // IPv4 (4) or IPv6 (6)
localAddress: '192.168.1.100', // Bind to specific IP
// TLS/SSL (https module)
cert: fs.readFileSync('cert.pem'),
key: fs.readFileSync('key.pem'),
rejectUnauthorized: true, // Validate server cert
};
4. HTTPS
For HTTPS requests, use the https module instead:
const https = require('https');
const http = require('http');
function createClient(urlStr) {
const url = new URL(urlStr);
const mod = url.protocol === 'https:' ? https : http;
return {
request(options, body) {
return new Promise((resolve, reject) => {
const mergedOptions = {
hostname: url.hostname,
port: url.port || (url.protocol === 'https:' ? 443 : 80),
path: url.pathname + url.search,
...options,
};
const req = mod.request(mergedOptions, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => resolve({
statusCode: res.statusCode,
headers: res.headers,
body: Buffer.concat(chunks).toString(),
}));
});
req.on('error', reject);
if (body) req.write(body);
req.end();
});
},
};
}
// Usage
const client = createClient('https://api.github.com');
const response = await client.request({
path: '/users/octocat',
headers: { 'User-Agent': 'Node.js' },
});
console.log(response.body);
5. The fetch API (Node.js 18+)
Node.js 18 introduced a global fetch based on undici:
// No import needed - it's global
// GET
const response = await fetch('https://api.github.com/users/octocat');
const data = await response.json();
console.log(data.login);
// POST
const createResponse = await fetch('https://api.example.com/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123',
},
body: JSON.stringify({ name: 'Alice' }),
});
if (createResponse.ok) {
const user = await createResponse.json();
console.log('Created:', user);
}
Advanced Fetch Patterns
// Error handling
async function fetchWithErrorHandling(url, options = {}) {
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(5000), // 5 second timeout
});
if (!response.ok) {
const errorBody = await response.text();
throw new Error(`HTTP ${response.status}: ${errorBody.slice(0, 200)}`);
}
return response;
} catch (err) {
if (err.name === 'AbortError') {
throw new Error('Request timed out');
}
throw err;
}
}
// Form data (Node.js 20+)
const form = new FormData();
form.append('username', 'alice');
form.append('avatar', new Blob([buffer]), 'photo.jpg');
const formResponse = await fetch('https://api.example.com/upload', {
method: 'POST',
body: form, // Content-Type is set automatically with boundary
});
Abort Controller - Cancelling Requests
function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeout));
}
// Or use AbortSignal.timeout() (Node.js 19+)
const response = await fetch(url, {
signal: AbortSignal.timeout(5000),
});
6. Streaming Responses (All Methods)
All HTTP client methods support streaming - useful for large responses:
const http = require('http');
const fs = require('fs');
const { pipeline } = require('stream/promises');
// Download a large file without buffering in memory
async function downloadFile(url, destPath) {
return new Promise((resolve, reject) => {
http.get(url, async (response) => {
if (response.statusCode !== 200) {
reject(new Error(`HTTP ${response.statusCode}`));
response.resume(); // Drain the response
return;
}
const totalSize = parseInt(response.headers['content-length'], 10);
let downloaded = 0;
// Track progress
response.on('data', (chunk) => {
downloaded += chunk.length;
const percent = ((downloaded / totalSize) * 100).toFixed(1);
process.stdout.write(`\rDownloading: ${percent}%`);
});
await pipeline(response, fs.createWriteStream(destPath));
console.log('\nDownload complete:', destPath);
resolve();
}).on('error', reject);
});
}
7. Retry with Exponential Backoff
Network calls fail. Retry intelligently:
async function fetchWithRetry(url, options = {}) {
const {
retries = 3,
baseDelay = 1000,
maxDelay = 10000,
shouldRetry = (response) => response.status >= 500 || response.status === 429,
} = options;
for (let attempt = 1; attempt <= retries; attempt++) {
try {
const response = await fetch(url);
if (response.ok) return response;
if (shouldRetry(response) && attempt < retries) {
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
console.warn(`Attempt ${attempt} returned ${response.status}. Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
continue;
}
// Non-retryable error or last attempt
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (err) {
if (err.name === 'AbortError') throw err;
if (attempt === retries) throw err;
// Network error - retry
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
console.warn(`Attempt ${attempt} failed: ${err.message}. Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
8. Connection Pooling (Keep-Alive)
By default, Node.js creates a new TCP connection for each request. Enable keep-alive for reuse:
const http = require('http');
const { Agent } = require('http');
// Create a connection pool
const agent = new Agent({
keepAlive: true,
keepAliveMsecs: 1000,
maxSockets: 50, // Max concurrent connections per host
maxFreeSockets: 10, // Max idle sockets to keep open
timeout: 30000, // Socket inactivity timeout
});
async function makeRequest(path) {
// All requests through this agent share connections
return new Promise((resolve, reject) => {
const req = http.request({
hostname: 'api.example.com',
path,
agent, // Share the agent
}, (res) => {
let data = '';
res.on('data', c => data += c);
res.on('end', () => resolve(data));
});
req.on('error', reject);
req.end();
});
}
// Make 100 requests - they share TCP connections
const promises = Array.from({ length: 100 }, (_, i) =>
makeRequest(`/items/${i}`)
);
const results = await Promise.all(promises);
Practical: API Client Class
const https = require('https');
class APIClient {
constructor(baseURL, options = {}) {
this.baseURL = new URL(baseURL);
this.headers = {
'Content-Type': 'application/json',
'User-Agent': 'APIClient/1.0',
...options.headers,
};
this.timeout = options.timeout || 10000;
}
async request(method, path, body = null) {
const url = new URL(path, this.baseURL);
return new Promise((resolve, reject) => {
const req = https.request(
{
hostname: url.hostname,
port: url.port,
path: url.pathname + url.search,
method,
headers: {
...this.headers,
...(body ? { 'Content-Length': Buffer.byteLength(body) } : {}),
},
timeout: this.timeout,
},
(res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
const data = Buffer.concat(chunks).toString();
if (res.statusCode >= 400) {
reject(new APIError(
res.statusCode,
res.statusMessage,
data,
));
return;
}
resolve({
status: res.statusCode,
headers: res.headers,
data: data ? JSON.parse(data) : null,
});
});
},
);
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
if (body) req.write(body);
req.end();
});
}
// Convenience methods
get(path) { return this.request('GET', path); }
post(path, data) { return this.request('POST', path, JSON.stringify(data)); }
put(path, data) { return this.request('PUT', path, JSON.stringify(data)); }
delete(path) { return this.request('DELETE', path); }
}
class APIError extends Error {
constructor(status, message, body) {
super(`${status}: ${message}`);
this.status = status;
this.body = body;
}
}
// Usage
const client = new APIClient('https://jsonplaceholder.typicode.com');
const posts = await client.get('/posts');
console.log('Posts:', posts.data.slice(0, 2));
const newPost = await client.post('/posts', {
title: 'My Post',
body: 'Content here',
userId: 1,
});
console.log('Created:', newPost.data);
Key Takeaways
http.get()- simplest for GET requests, callsreq.end()automaticallyhttp.request()- full control over method, headers, body - requires manualreq.end()- Always handle errors - network failures, timeouts, DNS errors, and non-2xx status codes
- Set timeouts -
req.setTimeout()orAbortSignal.timeout()prevents hanging requests - Check
res.statusCode- redirects (3xx), errors (4xx, 5xx), success (2xx) - Stream large responses - use
pipeline(response, fs.createWriteStream())for files - Use keep-alive agents for connection reuse when making many requests to the same host
fetch()(Node 18+) is the modern, promise-based alternative - prefer it for new code- Implement retry with exponential backoff for flaky networks or rate-limited APIs
- Build an API client class to encapsulate base URL, headers, auth, and error handling
Keep learning
Related tutorials from across the library.
Building an HTTP Server with Core http Module
Build production-ready HTTP servers with Node.js core http module - from hello world to routing, middleware, and static files
Web Development / Backend / Node.js / Web Server
Handling GET, POST, PUT, DELETE Requests
Implement full CRUD with Node.js HTTP server - GET, POST, PUT, DELETE methods, status codes, and RESTful conventions
Web Development / Backend / Node.js / Web Server
Parsing Request Body Data
Master HTTP request body parsing - JSON, URL-encoded, multipart/form-data, file uploads, streaming parsers, and validation
Web Development / Backend / Node.js / Web Server