Skip to main content
JavaScript 4 mins read Devs2.org

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.

#JavaScript #Fetch API #HTTP #REST #AJAX #Frontend #Async

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.

javascript-fetch-api-complete-guide

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ínhMô tả
response.okBoolean, true nếu status 200-299
response.statusMã trạng thái HTTP (200, 404, 500…)
response.statusTextVăn bản mô tả trạng thái (“OK”, “Not Found”)
response.headersHeadersResponse object chứa response headers
response.urlURL của request
response.typeLoại response (“basic”, “cors”, “opaque”)
response.bodyReadableStream 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ăngFetch APIAxiosXMLHttpRequest
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 AbortControllertimeout: 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 size0 KB~13 KB gzip0 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ớ:

  1. Luôn kiểm tra response.ok — Fetch không throw error cho HTTP 4xx/5xx
  2. Dùng AbortController để hủy request, tránh memory leak và race condition
  3. Tạo wrapper chung để tái sử dụng logic authentication, error handling, timeout
  4. Hiểu rõ sự khác biệt giữa network error và HTTP error
  5. 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!

Recently Used Tools