JavaScript Fetch API - Hướng Dẫn Toàn Diện Về Gọi API Từ Browser
Tìm hiểu cách sử dụng Fetch API để gọi HTTP requests trong JavaScript, xử lý JSON, error handling, AbortController, và so sánh với XMLHttpRequest và Axios.
Khi xây dựng ứng dụng web hiện đại, khả năng giao tiếp với server mà không cần reload trang là yếu tố sống còn. Từ AJAX ngày xưa đến Fetch API hôm nay, cách chúng ta gọi API đã tiến hóa đáng kể — và Fetch API chính là tiêu chuẩn hiện đại nhất mà mọi frontend developer cần nắm vững.
Trong bài viết này, bạn sẽ học toàn diện về Fetch API: cú pháp cơ bản, các phương thức HTTP, xử lý JSON, error handling chuyên nghiệp, AbortController để hủy request, và so sánh thực tế với các giải pháp thay thế.
Fetch API là gì?
Fetch API là một interface hiện đại được tích hợp sẵn trong trình duyệt, cho phép thực hiện HTTP requests (GET, POST, PUT, DELETE…) một cách bất đồng bộ. Nó dựa trên Promise, giúp code sạch hơn và dễ đọc hơn so với XMLHttpRequest (XHR) truyền thống.
Tại sao Fetch API ra đời?
Trước khi có Fetch, developers phải dùng XMLHttpRequest — một API cũ kỹ, callback-based, và khá cồng kềnh:
// ❌ XMLHttpRequest: Rườm rà, callback hell
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/users');
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const users = JSON.parse(xhr.responseText);
console.log(users);
} else {
console.error('Lỗi:', xhr.status);
}
}
};
xhr.send();
Với Fetch API, cùng tác vụ đó trở nên gọn gàng hơn hẳn:
// ✅ Fetch API: Sạch sẽ, readable
try {
const response = await fetch('/api/users');
const users = await response.json();
console.log(users);
} catch (error) {
console.error('Lỗi:', error);
}
Cú pháp cơ bản của Fetch
Cú pháp đơn giản nhất của fetch() nhận một URL và trả về một Promise:
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Lỗi:', error));
Hoặc dùng async/await (khuyên dùng):
async function loadData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
console.log(data);
} catch (error) {
console.error('Lỗi:', error);
}
}
loadData();
Response Object
Khi fetch hoàn tất, bạn nhận được một Response object với các thuộc tính quan trọng:
| Thuộc tính | Mô tả |
|---|---|
response.ok | Boolean, true nếu status 200-299 |
response.status | Mã trạng thái HTTP (200, 404, 500…) |
response.statusText | Văn bản mô tả trạng thái (“OK”, “Not Found”) |
response.headers | HeadersResponse object chứa response headers |
response.url | URL của request |
response.type | Loại response (“basic”, “cors”, “opaque”) |
response.body | ReadableStream chứa body (cho streaming) |
Quan trọng: Fetch không reject promise cho HTTP error status (4xx, 5xx). Bạn phải tự kiểm tra:
const response = await fetch('/api/users/999');
if (!response.ok) {
// Status 404 vẫn vào đây, KHÔNG throw exception!
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
Các phương thức HTTP với Fetch
GET Request – Lấy dữ liệu
// GET cơ bản
const response = await fetch('/api/products');
const products = await response.json();
// GET với query parameters
const searchTerm = 'laptop';
const response = await fetch(`/api/products?search=${encodeURIComponent(searchTerm)}&page=1&limit=20`);
const { products, total } = await response.json();
// GET với headers tùy chỉnh
const response = await fetch('/api/products', {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
});
POST Request – Gửi dữ liệu
// POST với JSON body
const newUser = { name: 'Nguyễn Văn A', email: 'vana@example.com' };
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIs...'
},
body: JSON.stringify(newUser)
});
const result = await response.json();
console.log('User created:', result);
PUT/PATCH Request – Cập nhật dữ liệu
// PUT: Cập nhật toàn bộ resource
const response = await fetch('/api/users/42', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: 'Nguyễn Văn A',
email: 'newemail@example.com',
role: 'admin'
})
});
// PATCH: Cập nhật một phần resource
const patchResponse = await fetch('/api/users/42', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ role: 'editor' })
});
DELETE Request – Xóa dữ liệu
const response = await fetch('/api/users/42', {
method: 'DELETE'
});
if (response.ok) {
console.log('User deleted successfully');
}
Xử lý các định dạng response
JSON (phổ biến nhất)
const response = await fetch('/api/users');
const data = await response.json();
Text / HTML
const response = await fetch('/api/page-content');
const text = await response.text();
document.getElementById('content').innerHTML = text;
FormData (upload file)
// Upload file kèm metadata
const formData = new FormData();
formData.append('avatar', fileInput.files[0]);
formData.append('userId', '42');
formData.append('caption', 'Ảnh mới của tôi');
const response = await fetch('/api/upload', {
method: 'POST',
body: formData
// KHÔNG set Content-Type! Browser tự động set boundary
});
const result = await response.json();
Blob (download file)
// Tải file PDF về
const response = await fetch('/api/invoice/123.pdf');
const blob = await response.blob();
// Tạo link download
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'invoice-123.pdf';
a.click();
URL.revokeObjectURL(url);
Error Handling chuyên nghiệp
Đây là phần quan trọng nhất khi làm việc với Fetch API.
Lỗi mạng vs Lỗi HTTP
// ⚠️ Phân biệt rõ hai loại lỗi:
// 1. Network error: Không có kết nối, DNS fail, CORS block → fetch THROWS
// 2. HTTP error: Server trả về 404, 500 → fetch RESOLVES, nhưng response.ok = false
async function safeFetch(url) {
try {
const response = await fetch(url);
// Kiểm tra HTTP status
if (!response.ok) {
const errorBody = await response.text();
throw new HttpError(response.status, response.statusText, errorBody);
}
return await response.json();
} catch (error) {
// Network error hoặc HttpError từ trên
if (error.name === 'HttpError') {
console.error(`Server error ${error.status}: ${error.message}`);
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
console.error('Không có kết nối mạng!');
} else {
console.error('Lỗi không xác định:', error);
}
throw error;
}
}
class HttpError extends Error {
constructor(status, statusText, body) {
super(`HTTP ${status}: ${statusText}`);
this.name = 'HttpError';
this.status = status;
this.statusText = statusText;
this.body = body;
}
}
Retry mechanism – Tự động thử lại
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (!response.ok) {
// Chỉ retry với các status code transient errors
if ([500, 502, 503, 504].includes(response.status) && attempt < maxRetries) {
const delay = Math.pow(2, attempt) * 1000; // Exponential backoff
console.warn(`Attempt ${attempt} failed (${response.status}), retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
lastError = error;
// Không retry network error sau lần đầu
if (attempt < maxRetries && error.name === 'TypeError') {
const delay = Math.pow(2, attempt) * 1000;
console.warn(`Network error, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
continue;
}
break;
}
}
throw lastError;
}
// Usage
const data = await fetchWithRetry('/api/unstable-endpoint', {}, 3);
Parallel requests với Promise.allSettled
// Chạy nhiều request song song, xử lý graceful khi một số thất bại
async function loadDashboardData() {
const urls = [
'/api/stats',
'/api/recent-orders',
'/api/notifications',
'/api/user-profile'
];
const responses = await Promise.allSettled(
urls.map(url => fetch(url).then(r => r.json()))
);
const results = responses.map((result, index) => ({
url: urls[index],
status: result.status,
data: result.status === 'fulfilled' ? result.value : null,
error: result.status === 'rejected' ? result.reason : null
}));
// Render dashboard với dữ liệu có sẵn, hiển thị lỗi cho phần tử thất bại
results.forEach(({ url, status, data, error }) => {
if (status === 'fulfilled') {
renderSection(url, data);
} else {
renderErrorSection(url, error);
}
});
}
AbortController – Hủy request đang chạy
Khi user navigate nhanh hoặc search input có debounce, bạn cần hủy các request cũ để tránh waste bandwidth và race conditions.
// Debounced search với AbortController
let currentController = null;
async function searchProducts(query) {
// Hủy request cũ nếu còn đang chạy
if (currentController) {
currentController.abort();
}
// Tạo controller mới
currentController = new AbortController();
const { signal } = currentController;
try {
const response = await fetch(`/api/products?q=${encodeURIComponent(query)}`, {
signal // Truyền signal vào fetch
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const products = await response.json();
renderSearchResults(products);
} catch (error) {
if (error.name === 'AbortError') {
// Request bị hủy do debounce — bỏ qua, không log error
return;
}
console.error('Search failed:', error);
}
}
// Debounce helper
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Usage
const debouncedSearch = debounce(searchProducts, 300);
searchInput.addEventListener('input', (e) => debouncedSearch(e.target.value));
AbortController với timeout
async function fetchWithTimeout(url, timeoutMs = 5000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(url, {
signal: controller.signal
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error(`Request timeout after ${timeoutMs}ms`);
}
throw error;
}
}
// Usage
const data = await fetchWithTimeout('/api/slow-endpoint', 3000);
Streaming với Fetch API
Fetch API hỗ trợ ReadableStream, cho phép xử lý dữ liệu lớn từng phần thay vì chờ tải toàn bộ:
// Stream response text lớn
async function streamResponse(url) {
const response = await fetch(url);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let result = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
result += chunk;
// Xử lý từng chunk ngay lập tức
updateProgressBar(result.length);
displayPartialContent(chunk);
}
return result;
}
Streaming cho AI/LLM responses (SSE-like)
async function streamAIResponse(prompt) {
const response = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt })
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let fullResponse = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
fullResponse += chunk;
// Hiển thị realtime (giống typing effect)
displayStreamingText(fullResponse);
}
return fullResponse;
}
Custom Fetch Wrapper
Trong thực tế, bạn nên tạo một wrapper chung để tái sử dụng:
// api/client.js
class ApiClient {
constructor(baseURL = '', defaults = {}) {
this.baseURL = baseURL;
this.defaults = {
headers: defaults.headers || {},
timeout: defaults.timeout || 10000,
...defaults
};
}
_buildUrl(path) {
return `${this.baseURL}${path}`;
}
_createAbortSignal(timeout) {
const controller = new AbortController();
if (timeout) {
setTimeout(() => controller.abort(), timeout);
}
return controller.signal;
}
async request(path, options = {}) {
const url = this._buildUrl(path);
const config = {
...this.defaults,
...options,
headers: {
...this.defaults.headers,
...options.headers
}
};
config.signal = this._createAbortSignal(config.timeout);
try {
const response = await fetch(url, config);
if (!response.ok) {
const errorBody = await response.text().catch(() => '');
throw new ApiError(response.status, response.statusText, errorBody);
}
// Handle empty responses (204 No Content)
if (response.status === 204) return null;
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new ApiError(408, 'Request Timeout');
}
throw error;
}
}
get(path, options) { return this.request(path, { ...options, method: 'GET' }); }
post(path, body, options) {
return this.request(path, {
...options,
method: 'POST',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(body)
});
}
put(path, body, options) {
return this.request(path, {
...options,
method: 'PUT',
headers: { 'Content-Type': 'application/json', ...options?.headers },
body: JSON.stringify(body)
});
}
delete(path, options) { return this.request(path, { ...options, method: 'DELETE' }); }
}
class ApiError extends Error {
constructor(status, statusText, body = '') {
super(`API Error ${status}: ${statusText}`);
this.name = 'ApiError';
this.status = status;
this.statusText = statusText;
this.body = body;
}
}
export default ApiClient;
Cách sử dụng:
import ApiClient from './api/client';
const api = new ApiClient('https://api.example.com', {
headers: { 'Authorization': 'Bearer YOUR_TOKEN' }
});
// GET
const users = await api.get('/users');
// POST
const newUser = await api.post('/users', { name: 'John', email: 'john@example.com' });
// PUT
const updated = await api.put('/users/42', { role: 'admin' });
// DELETE
await api.delete('/users/42');
Fetch vs Axios vs XMLHttpRequest
| Tính năng | Fetch API | Axios | XMLHttpRequest |
|---|---|---|---|
| Built-in browser | ✅ Có | ❌ Cần install | ✅ Có (cũ) |
| Promise-based | ✅ Có | ✅ Có | ❌ Callback |
| Auto JSON parse | ❌ Phải gọi .json() | ✅ Tự động | ❌ Phải JSON.parse() |
| Interceptors | ❌ Không có | ✅ Có | ❌ Không có |
| Timeout config | ❌ Cần AbortController | ✅ timeout: ms | ❌ Cần timeout riêng |
| Progress tracking | ✅ Stream API | ✅ Có | ✅ Có |
| Client-side default | ✅ Luôn có | Phổ biến nhất | Đã deprecated |
| Bundle size | 0 KB | ~13 KB gzip | 0 KB (cũ) |
| Node.js support | ✅ (Node 18+) | ✅ Có | ❌ Không |
Khi nào dùng cái nào?
- Fetch API: Dự án muốn zero-dependency, đã quen async/await, không cần interceptor
- Axios: Dự án lớn cần interceptor, auto-transform, hỗ trợ cả browser và Node.js
- XMLHttpRequest: Chỉ maintain legacy code — không nên dùng trong project mới
Best Practices
1. Luôn bao bọc trong try/catch
// ❌ BAD: Bỏ qua error handling
const response = await fetch('/api/data');
const data = await response.json();
// ✅ GOOD: Xử lý đầy đủ
try {
const response = await fetch('/api/data');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
} catch (error) {
console.error('Fetch failed:', error);
}
2. Dùng AbortController cho navigation
// Trong React component
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal).catch(err => {
if (err.name !== 'AbortError') handleError(err);
});
return () => controller.abort(); // Cleanup khi unmount
}, []);
3. Cache strategy phù hợp
// Cache-first: Ưu tiên cache, fallback network
async function cachedFetch(url, cacheName = 'api-cache', ttl = 5 * 60 * 1000) {
// Thử từ cache trước
const cached = caches.match(url);
if (cached) {
const { data, timestamp } = await cached.json();
if (Date.now() - timestamp < ttl) {
return data; // Cache còn hạn
}
}
// Fetch từ network
const response = await fetch(url);
const data = await response.json();
// Lưu vào cache
const cache = await caches.open(cacheName);
await cache.put(url, new Response(JSON.stringify({ data, timestamp: Date.now() })));
return data;
}
4. Tránh memory leak
// ❌ BAD: Không cleanup listener
fetch('/api/stream')
.then(r => r.body.getReader())
.then(reader => {
reader.read().then(process);
// Nếu component unmount, reader vẫn chạy → memory leak
});
// ✅ GOOD: Cleanup đúng cách
let reader = null;
async function startStream() {
const response = await fetch('/api/stream');
reader = response.body.getReader();
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
processChunk(value);
}
} finally {
reader.releaseLock(); // Giải phóng reader
}
}
// Cleanup khi unmount
stopStream = () => {
if (reader) {
reader.cancel();
reader = null;
}
};
Kết luận
Fetch API là công cụ mạnh mẽ và hiện đại để giao tiếp với server trong browser. Những điểm mấu chốt cần nhớ:
- Luôn kiểm tra
response.ok— Fetch không throw error cho HTTP 4xx/5xx - Dùng AbortController để hủy request, tránh memory leak và race condition
- Tạo wrapper chung để tái sử dụng logic authentication, error handling, timeout
- Hiểu rõ sự khác biệt giữa network error và HTTP error
- Xem xét Axios nếu project cần interceptor, auto-transform, hoặc đa-platform
Việc nắm vững Fetch API không chỉ giúp bạn gọi API hiệu quả hơn mà còn hiểu sâu hơn về cách browser xử lý HTTP requests — kiến thức nền tảng cho mọi frontend developer hiện đại.
Bạn thường dùng Fetch API hay Axios cho project của mình? Chia sẻ kinh nghiệm trong phần bình luận bên dưới!