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: 'alice@example.com',
});
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