Making HTTP Requests (Client-Side)

📖 Concept

Node.js can act as both an HTTP server and client. Making outbound HTTP requests is essential for calling APIs, microservice communication, and web scraping.

Built-in options:

Method Node.js Version Style
http.request() / https.request() All Callback + streams
fetch() 18+ (stable in 21+) Promise-based (Web standard)

Third-party libraries:

Library Pros Use Case
axios Feature-rich, interceptors, auto-transform Most popular choice
node-fetch Lightweight fetch polyfill Minimal projects
got Retry, pagination, hooks Advanced use cases
undici Fastest, powers Node.js fetch Performance-critical

fetch() in Node.js 18+: Node.js now includes the standard fetch() API, powered by the undici HTTP client. It works exactly like browser fetch — this is the recommended approach for new projects.

Best practices for HTTP clients:

  1. Set timeouts — Never make a request without a timeout
  2. Handle errors — Network errors, timeouts, non-2xx responses
  3. Retry with backoff — For transient failures (5xx, network errors)
  4. Use AbortController — Cancel requests that are no longer needed
  5. Connection pooling — Reuse TCP connections for repeated calls to the same host

🏠 Real-world analogy: If your HTTP server is a restaurant kitchen, the HTTP client is like a delivery driver — it goes out to other restaurants (APIs) to pick up ingredients (data) and bring them back.

💻 Code Example

codeTap to expand ⛶
1// HTTP Client — Making Requests from Node.js
2
3// 1. Built-in fetch (Node.js 18+)
4async function fetchExample() {
5 // GET request
6 const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
7
8 if (!response.ok) {
9 throw new Error(`HTTP ${response.status}: ${response.statusText}`);
10 }
11
12 const user = await response.json();
13 console.log("User:", user.name);
14
15 // POST request
16 const newPost = await fetch("https://jsonplaceholder.typicode.com/posts", {
17 method: "POST",
18 headers: { "Content-Type": "application/json" },
19 body: JSON.stringify({
20 title: "My Post",
21 body: "Hello from Node.js",
22 userId: 1,
23 }),
24 });
25
26 const created = await newPost.json();
27 console.log("Created post:", created);
28}
29
30// 2. Fetch with timeout using AbortController
31async function fetchWithTimeout(url, timeoutMs = 5000) {
32 const controller = new AbortController();
33 const timeout = setTimeout(() => controller.abort(), timeoutMs);
34
35 try {
36 const response = await fetch(url, { signal: controller.signal });
37 return await response.json();
38 } catch (err) {
39 if (err.name === "AbortError") {
40 throw new Error(`Request to ${url} timed out after ${timeoutMs}ms`);
41 }
42 throw err;
43 } finally {
44 clearTimeout(timeout);
45 }
46}
47
48// 3. Retry with exponential backoff
49async function fetchWithRetry(url, options = {}) {
50 const { maxRetries = 3, baseDelay = 1000, ...fetchOptions } = options;
51
52 for (let attempt = 1; attempt <= maxRetries; attempt++) {
53 try {
54 const response = await fetch(url, fetchOptions);
55
56 if (response.status >= 500) {
57 throw new Error(`Server error: ${response.status}`);
58 }
59 if (!response.ok) {
60 throw new Error(`HTTP ${response.status}`);
61 }
62
63 return await response.json();
64 } catch (err) {
65 if (attempt === maxRetries) throw err;
66
67 const delay = baseDelay * Math.pow(2, attempt - 1);
68 console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
69 await new Promise((r) => setTimeout(r, delay));
70 }
71 }
72}
73
74// 4. Parallel API calls
75async function fetchDashboardData() {
76 const [users, posts, comments] = await Promise.all([
77 fetch("https://jsonplaceholder.typicode.com/users").then((r) => r.json()),
78 fetch("https://jsonplaceholder.typicode.com/posts").then((r) => r.json()),
79 fetch("https://jsonplaceholder.typicode.com/comments").then((r) => r.json()),
80 ]);
81
82 return {
83 totalUsers: users.length,
84 totalPosts: posts.length,
85 totalComments: comments.length,
86 };
87}
88
89// 5. Built-in http module (lower level)
90const https = require("https");
91
92function httpGet(url) {
93 return new Promise((resolve, reject) => {
94 https.get(url, (res) => {
95 let data = "";
96 res.on("data", (chunk) => (data += chunk));
97 res.on("end", () => {
98 try {
99 resolve(JSON.parse(data));
100 } catch {
101 resolve(data);
102 }
103 });
104 }).on("error", reject);
105 });
106}
107
108// 6. API client wrapper with base URL and auth
109class APIClient {
110 constructor(baseURL, options = {}) {
111 this.baseURL = baseURL;
112 this.defaultHeaders = {
113 "Content-Type": "application/json",
114 ...options.headers,
115 };
116 this.timeout = options.timeout || 10000;
117 }
118
119 async request(method, path, body = null) {
120 const controller = new AbortController();
121 const timeout = setTimeout(() => controller.abort(), this.timeout);
122
123 try {
124 const response = await fetch(`${this.baseURL}${path}`, {
125 method,
126 headers: this.defaultHeaders,
127 body: body ? JSON.stringify(body) : null,
128 signal: controller.signal,
129 });
130
131 if (!response.ok) {
132 const error = await response.text();
133 throw new Error(`${method} ${path} failed: ${response.status} - ${error}`);
134 }
135
136 return response.status === 204 ? null : await response.json();
137 } finally {
138 clearTimeout(timeout);
139 }
140 }
141
142 get(path) { return this.request("GET", path); }
143 post(path, body) { return this.request("POST", path, body); }
144 put(path, body) { return this.request("PUT", path, body); }
145 delete(path) { return this.request("DELETE", path); }
146}
147
148// Usage
149// const api = new APIClient("https://api.example.com", {
150// headers: { Authorization: "Bearer token123" },
151// timeout: 5000,
152// });
153// const users = await api.get("/users");
154// const newUser = await api.post("/users", { name: "Alice" });

🏋️ Practice Exercise

Exercises:

  1. Build an API client wrapper with base URL, default headers, and timeout support
  2. Implement fetchWithRetry() with exponential backoff and configurable max retries
  3. Use Promise.all() to call 3 APIs in parallel and merge the results
  4. Create a simple web scraper that fetches a URL and extracts all links from the HTML
  5. Implement request caching — cache GET responses for 5 minutes, bypass for POST/PUT
  6. Build a proxy server that forwards requests to an upstream API and adds authentication headers

⚠️ Common Mistakes

  • Not setting timeouts on outbound requests — a hanging upstream service can exhaust your connection pool and freeze your entire application

  • Not checking response.ok after fetch — fetch resolves for 4xx and 5xx responses; only network failures reject the Promise

  • Making sequential API calls when they could run in parallel — use Promise.all() for independent requests

  • Hardcoding API URLs — use environment variables for base URLs so they can change between development, staging, and production

  • Not handling AbortController cleanup — always clear the timeout in a finally block to prevent memory leaks

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Making HTTP Requests (Client-Side). Login to unlock this feature.