Before diving into the examples, ensure you have:
node -v
)The Fetch API follows a simple, intuitive syntax:
const response = await fetch(url[, options]); const data = await response.json();
Let's start with a basic GET request:
async function getUser() { try { const response = await fetch('https://api.github.com/users/octocat'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); console.log(data); } catch (error) { console.error('Failed to fetch:', error); } } getUser();
// Basic GET request const response = await fetch('https://api.example.com/users'); const users = await response.json(); // GET request with query parameters const username = 'john'; const response = await fetch(`https://api.example.com/users?name=${encodeURIComponent(username)}`); const user = await response.json();
const createUser = async (userData) => { const response = await fetch('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(userData) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); }; // Usage const newUser = await createUser({ name: 'John Doe', email: '[email protected]' });
// PUT request const updateUser = async (userId, userData) => { const response = await fetch(`https://api.example.com/users/${userId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(userData) }); return await response.json(); }; // PATCH request const partialUpdate = async (userId, updates) => { const response = await fetch(`https://api.example.com/users/${userId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updates) }); return await response.json(); };
Understanding how to properly work with HTTP headers is crucial for effective API interactions:
const response = await fetch('https://api.example.com/data', { headers: { 'Authorization': 'Bearer your-token-here', 'Accept': 'application/json', 'X-Custom-Header': 'custom-value' } }); // Reading response headers const contentType = response.headers.get('content-type'); const customHeader = response.headers.get('x-custom-header'); // Iterating over headers for (const [key, value] of response.headers) { console.log(`${key}: ${value}`); }
// Download large file as stream const downloadFile = async (url, targetFile) => { const response = await fetch(url); const fileStream = fs.createWriteStream(targetFile); for await (const chunk of response.body) { fileStream.write(chunk); } fileStream.end(); }; // Upload file as stream const uploadFile = async (url, fileStream) => { const response = await fetch(url, { method: 'POST', body: fileStream, duplex: 'half' }); return response.ok; };
Implementing robust error handling is crucial for reliable applications:
class FetchError extends Error { constructor(message, status, data) { super(message); this.name = 'FetchError'; this.status = status; this.data = data; } } async function safeFetch(url, options = {}) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), options.timeout || 5000); try { const response = await fetch(url, { ...options, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const errorData = await response.json().catch(() => null); throw new FetchError( `HTTP error! status: ${response.status}`, response.status, errorData ); } return await response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof FetchError) { throw error; } if (error.name === 'AbortError') { throw new FetchError('Request timed out', 408, null); } throw new FetchError('Network error', 0, error.message); } }
Implementing a proper retry mechanism can significantly improve your application's reliability:
async function fetchWithRetry(url, options = {}, maxRetries = 3) { let lastError; for (let i = 0; i < maxRetries; i++) { try { return await safeFetch(url, options); } catch (error) { lastError = error; if (error.status && error.status < 500) { // Don't retry client errors throw error; } // Exponential backoff await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); } } throw lastError; }
import { Agent } from 'undici'; const agent = new Agent({ keepAliveTimeout: 60000, keepAliveMaxTimeout: 60000, connections: 10 }); const response = await fetch('https://api.example.com', { dispatcher: agent });
async function batchRequests(urls, batchSize = 5) { const results = []; for (let i = 0; i < urls.length; i += batchSize) { const batch = urls.slice(i, i + batchSize); const promises = batch.map(url => safeFetch(url)); const batchResults = await Promise.allSettled(promises); results.push(...batchResults); } return results; }
// Axios const response = await axios.get(url, { headers: { Authorization: 'Bearer token' } }); const data = response.data; // Fetch const response = await fetch(url, { headers: { Authorization: 'Bearer token' } }); const data = await response.json(); // Axios axios.post(url, data, { headers: { 'Content-Type': 'application/json' } }); // Fetch fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
Technical discussions across various platforms reveal interesting insights about how the Node.js Fetch API is being received in real-world applications. One particularly notable aspect that developers appreciate is the API's clear separation between network-level and application-level errors. Engineers point out that this design choice aligns well with HTTP's architectural principles - a 404 or 500 response represents a successful HTTP transaction, even if it indicates an application-level error.
The developer community particularly values the API's simplicity and composability. Several senior engineers highlight how Fetch's function-based approach makes it easy to wrap and extend, unlike more complex class-based HTTP clients. This simplicity also translates into better testing capabilities, as mocking a single function is straightforward. However, some teams note that this minimalist approach means writing additional error handling code, particularly for those transitioning from libraries like Axios that automatically reject promises on non-200 status codes.
Another significant theme in community discussions is the API's role in code portability between browser and Node.js environments. Development teams report fewer configuration issues and reduced dependency management challenges when using the native Fetch API. This is especially valuable for full-stack applications and libraries that need to work in both environments. However, some developers point out that differences still exist, particularly around features like CORS and cookie handling, which need to be carefully considered in cross-environment code.
The integration with Node's streaming capabilities has also received positive attention from the community, particularly for handling large files and real-time data. However, developers working on more complex applications note that additional utilities are often needed for common tasks like request timeouts, retries, and advanced error handling, leading many teams to build their own utility wrappers around the basic Fetch API. These capabilities make it particularly useful for tasks like web scraping and data processing.
The Node.js Fetch API represents a significant step forward in standardizing HTTP requests across JavaScript environments. Its native implementation in Node.js 18+ provides excellent performance through the Undici engine while maintaining a familiar Promise-based interface that developers know from browser environments.
While some developers may initially find its error handling approach different from other HTTP clients, the clear separation between network and application errors aligns well with HTTP principles and promotes better error handling practices. The API's simplicity and extensibility make it an excellent choice for both simple scripts and complex applications.