Fetch API là gì? Hướng dẫn sử dụng Fetch để gửi và nhận dữ liệu

Trong các website hiện đại, việc gửi và nhận dữ liệu giữa trình duyệt với máy chủ diễn ra liên tục. Khi người dùng đăng nhập, tìm kiếm sản phẩm, gửi biểu mẫu, tải thêm nội dung hoặc cập nhật một thông tin nào đó mà không muốn tải lại toàn bộ trang, JavaScript cần một cơ chế giao tiếp với máy chủ phù hợp. Fetch API là một trong những giải pháp phổ biến nhất cho nhu cầu này.

Fetch cho phép JavaScript tạo HTTP request và xử lý response trực tiếp trong trình duyệt. So với cách tiếp cận cũ bằng XMLHttpRequest, Fetch có cú pháp gọn hơn, hoạt động tự nhiên với Promise và đặc biệt phù hợp với JavaScript hiện đại.

Trong bài viết này, Web Mới sẽ đi từ nền tảng đến thực tế: Fetch API hoạt động như thế nào, cấu trúc một request ra sao, cách gửi dữ liệu bằng GET, POST, PUT, PATCH, DELETE, cách đọc JSON, xử lý lỗi và những điểm dễ sai khi xây dựng chức năng giao tiếp với server.

Fetch API là gì? Hướng dẫn sử dụng Fetch để gửi và nhận dữ liệu
Fetch API là gì? Hướng dẫn sử dụng Fetch để gửi và nhận dữ liệu

Fetch API là gì và vì sao được sử dụng phổ biến?

Fetch API là giao diện JavaScript được trình duyệt cung cấp để thực hiện các HTTP request. Thông qua Fetch, mã JavaScript có thể yêu cầu dữ liệu từ một URL, gửi dữ liệu lên server và nhận kết quả trả về mà không nhất thiết phải tải lại trang.

Điểm quan trọng là Fetch không phải một thư viện bên ngoài. Trong môi trường trình duyệt hiện đại, Fetch là một Web API có sẵn, vì vậy thông thường bạn không cần cài thêm thư viện chỉ để thực hiện một request cơ bản.

Một lời gọi Fetch đơn giản có thể bắt đầu như sau:

fetch('/api/products')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

Ở ví dụ trên, fetch() gửi request đến địa chỉ /api/products. Khi server phản hồi, chương trình tiếp tục đọc nội dung response dưới dạng JSON rồi xử lý dữ liệu nhận được.

Fetch đặc biệt hữu ích trong các tình huống như:

  • Lấy danh sách dữ liệu từ server.
  • Gửi thông tin biểu mẫu mà không cần tải lại trang.
  • Tạo, sửa hoặc xóa dữ liệu thông qua API.
  • Tìm kiếm dữ liệu theo từ khóa.
  • Tải thêm nội dung khi người dùng cuộn trang.
  • Cập nhật một phần giao diện sau khi thao tác thành công.
  • Kết nối giao diện JavaScript với backend PHP hoặc các hệ thống cung cấp REST API.

Cách Fetch xử lý một request và response

Để sử dụng Fetch hiệu quả, cần phân biệt hai giai đoạn: request là dữ liệu trình duyệt gửi đi và response là kết quả server trả về.

Khi gọi fetch(), trình duyệt bắt đầu thực hiện request. Hàm này trả về một Promise. Promise đó không chứa trực tiếp dữ liệu JSON mà thường hoàn thành với một đối tượng Response.

Có thể hình dung quy trình theo thứ tự:

  1. JavaScript gọi fetch().
  2. Trình duyệt tạo HTTP request.
  3. Request được gửi đến server.
  4. Server xử lý yêu cầu.
  5. Server trả về HTTP response.
  6. Fetch hoàn thành Promise với đối tượng Response.
  7. JavaScript đọc phần body của response.
  8. Dữ liệu được chuyển sang dạng phù hợp như JSON hoặc text.
  9. Chương trình sử dụng dữ liệu để cập nhật giao diện hoặc thực hiện logic tiếp theo.

Ví dụ, nếu API trả về JSON, việc đọc dữ liệu thường được thực hiện bằng:

fetch('/api/products')
  .then(response => response.json())
  .then(data => {
    console.log(data);
  });

Một điểm dễ hiểu nhầm là response.json() không đơn giản là lấy ra một biến JSON có sẵn. Đây là thao tác đọc body của response và chuyển nội dung đó thành dữ liệu JavaScript. Vì vậy, nó cũng trả về một Promise.

Promise trong Fetch có ý nghĩa gì?

Request mạng thường không hoàn thành ngay tại thời điểm JavaScript gọi hàm. Trình duyệt cần thời gian để kết nối, gửi request, chờ server xử lý và nhận response. Promise giúp JavaScript tiếp tục thực hiện các công việc khác trong thời gian chờ thay vì khóa toàn bộ giao diện.

Fetch có thể được viết bằng async/await, cách này thường dễ đọc hơn khi một chức năng có nhiều bước xử lý:

async function loadProducts() {
  const response = await fetch('/api/products');
  const data = await response.json();

  console.log(data);
}

loadProducts();

Với những chức năng thực tế, async/await thường giúp luồng xử lý gần với cách con người đọc: gửi request, chờ response, đọc dữ liệu rồi xử lý kết quả.

Cú pháp cơ bản của Fetch cần nắm

Ở dạng đơn giản nhất, Fetch nhận vào URL và tùy chọn trả về một Promise:

fetch(url, options);

Tham số đầu tiên là địa chỉ tài nguyên cần truy cập. Tham số thứ hai là một object tùy chọn dùng để điều khiển phương thức HTTP, header, body, credentials và nhiều thiết lập khác.

Ví dụ:

fetch('/api/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Nguyen Van A',
    email: 'a@example.com'
  })
});

Trong đó:

  • url: địa chỉ API hoặc tài nguyên cần gửi request.
  • method: phương thức HTTP như GET, POST, PUT, PATCH hoặc DELETE.
  • headers: các thông tin bổ sung gửi kèm request.
  • body: phần dữ liệu được gửi lên server, thường xuất hiện với POST, PUT hoặc PATCH.

Không phải request nào cũng cần truyền đầy đủ các thuộc tính trên. Với một request GET đơn giản, chỉ cần URL là đủ:

fetch('/api/users');

Gửi dữ liệu bằng Fetch với các phương thức HTTP

Fetch không giới hạn ở việc lấy dữ liệu. Nó có thể thực hiện nhiều loại HTTP request tùy theo mục đích của API. Việc chọn đúng method giúp frontend và backend thống nhất ý nghĩa của từng thao tác.

Lấy dữ liệu với GET

GET thường được sử dụng khi cần đọc dữ liệu từ server. Đây là trường hợp đơn giản nhất vì request thông thường không cần body.

async function getUsers() {
  const response = await fetch('/api/users');
  const data = await response.json();

  console.log(data);
}

getUsers();

Nếu cần truyền tham số tìm kiếm, dữ liệu thường được đặt trên URL:

const keyword = 'laptop';

fetch(`/api/products?keyword=${encodeURIComponent(keyword)}`)
  .then(response => response.json())
  .then(data => {
    console.log(data);
  });

Sử dụng encodeURIComponent() giúp giá trị tham số được mã hóa phù hợp khi đưa vào URL, đặc biệt khi từ khóa có khoảng trắng hoặc ký tự đặc biệt.

Gửi dữ liệu mới với POST

POST thường được dùng khi client muốn gửi dữ liệu để server tạo một tài nguyên hoặc thực hiện một thao tác xử lý.

Nếu API nhận JSON, cần khai báo Content-Type và chuyển object JavaScript thành chuỗi JSON bằng JSON.stringify():

async function createUser() {
  const user = {
    name: 'Nguyen Van A',
    email: 'a@example.com'
  };

  const response = await fetch('/api/users', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(user)
  });

  const data = await response.json();

  console.log(data);
}

createUser();

Đây là một điểm rất quan trọng: object JavaScript và chuỗi JSON không phải cùng một dạng dữ liệu. Khi API yêu cầu JSON, JSON.stringify() thường được dùng để chuyển object thành chuỗi trước khi đặt vào body.

Cập nhật dữ liệu với PUT và PATCH

PUTPATCH đều có thể được dùng để cập nhật dữ liệu, nhưng cách thiết kế API quyết định ý nghĩa cụ thể của từng method.

PUT thường được sử dụng khi client gửi một biểu diễn đầy đủ hơn của tài nguyên cần cập nhật:

const user = {
  name: 'Nguyen Van B',
  email: 'b@example.com'
};

const response = await fetch('/api/users/15', {
  method: 'PUT',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify(user)
});

PATCH phù hợp với trường hợp chỉ muốn thay đổi một phần dữ liệu:

const response = await fetch('/api/users/15', {
  method: 'PATCH',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Nguyen Van C'
  })
});

Không nên chỉ dựa vào tên method để quyết định cách gửi dữ liệu. Frontend cần tuân theo tài liệu API hoặc quy ước mà backend đã xây dựng.

Xóa dữ liệu với DELETE

DELETE thường được sử dụng khi muốn yêu cầu server xóa một tài nguyên:

const response = await fetch('/api/users/15', {
  method: 'DELETE'
});

if (response.ok) {
  console.log('Xóa thành công');
}

Một số API trả về JSON sau khi xóa, trong khi API khác có thể trả về response không có body. Vì vậy, không nên mặc định gọi response.json() cho mọi DELETE request nếu chưa biết định dạng response.

Nhận và đọc dữ liệu trả về từ server

Sau khi nhận được Response, bước tiếp theo là đọc body. Fetch hỗ trợ nhiều cách đọc dữ liệu tùy loại response.

Đọc JSON bằng response.json()

Đây là trường hợp phổ biến nhất khi làm việc với API:

const response = await fetch('/api/products');
const data = await response.json();

console.log(data);

Nếu server trả về:

{
  "id": 10,
  "name": "Laptop",
  "price": 15000000
}

thì sau khi gọi response.json(), JavaScript có thể truy cập dữ liệu theo thuộc tính:

console.log(data.id);
console.log(data.name);
console.log(data.price);

Đọc nội dung dạng văn bản

Nếu server trả về HTML, văn bản hoặc một nội dung không cần phân tích thành JSON, có thể sử dụng response.text():

const response = await fetch('/example.txt');
const content = await response.text();

console.log(content);

Việc chọn json() hay text() phụ thuộc vào nội dung thực tế mà server trả về. Gọi json() với một response không chứa JSON hợp lệ có thể khiến quá trình đọc dữ liệu phát sinh lỗi.

Kiểm tra HTTP status trước khi xử lý dữ liệu

Một trong những lỗi phổ biến khi mới sử dụng Fetch là cho rằng Promise chỉ thất bại khi server trả về mã HTTP như 404 hoặc 500. Trên thực tế, Fetch thường vẫn hoàn thành Promise với một Response khi server trả về các HTTP status lỗi. Vì vậy, cần chủ động kiểm tra response.ok hoặc response.status.

const response = await fetch('/api/products');

if (!response.ok) {
  throw new Error(`HTTP error: ${response.status}`);
}

const data = await response.json();

console.log(data);

response.ok cho biết response có nằm trong nhóm HTTP status thành công hay không. Cách kiểm tra này giúp chương trình không vô tình coi một response 404 hoặc 500 là dữ liệu hợp lệ.

Tùy chỉnh request bằng headers và body

Trong các ứng dụng thực tế, request thường cần nhiều thông tin hơn một URL. Header cho server biết cách diễn giải dữ liệu, loại nội dung đang gửi hoặc thông tin xác thực. Body chứa phần dữ liệu chính mà client muốn truyền đi.

Khai báo Content-Type khi gửi JSON

Khi gửi dữ liệu JSON, frontend nên thông báo rõ định dạng của body thông qua header Content-Type. Nếu bỏ qua thông tin này, backend có thể không nhận dữ liệu theo cách mà ứng dụng mong muốn.

const response = await fetch('/api/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    product_id: 25,
    quantity: 2
  })
});

const data = await response.json();

console.log(data);

Ở đây, object JavaScript được chuyển thành chuỗi JSON trước khi gửi. Backend sau đó có thể phân tích body dựa trên Content-Type và xử lý các trường dữ liệu tương ứng.

Gửi header tùy chỉnh

API đôi khi yêu cầu những header riêng, chẳng hạn token xác thực hoặc một giá trị nhận diện phiên làm việc.

const response = await fetch('/api/profile', {
  headers: {
    'Accept': 'application/json',
    'Authorization': 'Bearer YOUR_TOKEN'
  }
});

const data = await response.json();

console.log(data);

Không nên tự ý đưa thông tin nhạy cảm vào JavaScript phía trình duyệt. Những khóa bí mật hoặc credential có tính chất private không nên được xem như dữ liệu có thể bảo vệ bằng cách ẩn trong mã frontend.

Gửi dữ liệu biểu mẫu bằng Fetch

Fetch có thể kết hợp với phần tử form để xây dựng chức năng gửi biểu mẫu mà không cần tải lại toàn bộ trang. Cách này thường được dùng cho đăng nhập, đăng ký, liên hệ, tìm kiếm hoặc các biểu mẫu quản trị.

Giả sử biểu mẫu có cấu trúc:

<form id="contactForm">
  <input type="text" name="name" required>
  <input type="email" name="email" required>
  <textarea name="message" required></textarea>
  <button type="submit">Gửi</button>
</form>

Có thể lấy dữ liệu của form bằng FormData rồi gửi trực tiếp:

const form = document.querySelector('#contactForm');

form.addEventListener('submit', async function(event) {
  event.preventDefault();

  const formData = new FormData(form);

  const response = await fetch('/contact.php', {
    method: 'POST',
    body: formData
  });

  const data = await response.json();

  console.log(data);
});

Với FormData, không cần tự nối chuỗi các trường dữ liệu. Trình duyệt sẽ đảm nhiệm việc đóng gói dữ liệu theo định dạng phù hợp.

Khi nào nên dùng FormData thay vì JSON?

JSON phù hợp với các API nhận dữ liệu dạng cấu trúc rõ ràng. FormData lại đặc biệt hữu ích khi biểu mẫu có file upload, chẳng hạn ảnh đại diện, tài liệu hoặc hình ảnh sản phẩm.

Ví dụ:

const form = document.querySelector('#uploadForm');
const formData = new FormData(form);

const response = await fetch('/upload.php', {
  method: 'POST',
  body: formData
});

Khi sử dụng FormData, không nên tự đặt Content-Type: multipart/form-data trong hầu hết trường hợp. Trình duyệt cần tự thiết lập Content-Type kèm boundary tương ứng với dữ liệu multipart.

Viết Fetch bằng async/await cho code dễ quản lý

Promise chaining với then() vẫn hoàn toàn hợp lệ, nhưng khi một chức năng phải thực hiện nhiều bước liên tiếp, async/await thường giúp mã nguồn dễ đọc và dễ bảo trì hơn.

Thay vì:

fetch('/api/products')
  .then(response => {
    if (!response.ok) {
      throw new Error('Không thể lấy dữ liệu');
    }

    return response.json();
  })
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

Có thể viết:

async function loadProducts() {
  try {
    const response = await fetch('/api/products');

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    const data = await response.json();

    console.log(data);
  } catch (error) {
    console.error(error);
  }
}

loadProducts();

Cấu trúc này thể hiện khá rõ luồng xử lý: gọi API, kiểm tra kết quả HTTP, đọc dữ liệu và xử lý lỗi nếu có vấn đề.

Không quên bắt lỗi khi dùng await

Request mạng có thể gặp nhiều vấn đề như mất kết nối, DNS không hoạt động, request bị trình duyệt chặn hoặc quá trình đọc response thất bại. Vì vậy, những đoạn code quan trọng nên có cơ chế xử lý lỗi.

async function getData() {
  try {
    const response = await fetch('/api/data');

    if (!response.ok) {
      throw new Error(`Server trả về ${response.status}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Request thất bại:', error);
    return null;
  }
}

Điểm cần phân biệt là HTTP errornetwork error không hoàn toàn giống nhau. Một response có status 404 hoặc 500 vẫn có thể khiến fetch hoàn thành Promise. Ngược lại, một số lỗi mạng khiến Promise bị reject. Vì vậy, cách xử lý an toàn thường kết hợp cả try/catch và kiểm tra response.ok.

Xử lý dữ liệu API trước khi đưa lên giao diện

Fetch chỉ đảm nhiệm việc giao tiếp với server. Nó không tự động biến dữ liệu nhận được thành giao diện. Sau khi lấy dữ liệu, JavaScript vẫn cần kiểm tra cấu trúc và quyết định cách hiển thị.

Ví dụ API trả về danh sách sản phẩm:

[
  {
    "id": 1,
    "name": "Laptop A",
    "price": 15000000
  },
  {
    "id": 2,
    "name": "Laptop B",
    "price": 18000000
  }
]

Có thể duyệt dữ liệu và tạo nội dung hiển thị:

async function renderProducts() {
  const response = await fetch('/api/products');

  if (!response.ok) {
    throw new Error('Không thể tải sản phẩm');
  }

  const products = await response.json();
  const container = document.querySelector('#products');

  container.textContent = '';

  products.forEach(product => {
    const item = document.createElement('div');

    const name = document.createElement('h3');
    name.textContent = product.name;

    const price = document.createElement('p');
    price.textContent = `${product.price.toLocaleString('vi-VN')} đ`;

    item.appendChild(name);
    item.appendChild(price);
    container.appendChild(item);
  });
}

renderProducts();

Cách tạo phần tử và sử dụng textContent như trên có lợi thế là dữ liệu nhận từ server không được trình duyệt diễn giải trực tiếp như HTML. Đây là một thói quen đáng chú ý khi dữ liệu có nguồn từ người dùng hoặc nguồn bên ngoài.

Gửi dữ liệu tới backend PHP bằng Fetch

Fetch không phụ thuộc vào ngôn ngữ backend. Nếu server được xây dựng bằng PHP và cung cấp endpoint phù hợp, JavaScript có thể gửi request tới endpoint đó giống như với bất kỳ API nào khác.

Ví dụ frontend gửi JSON đến một file PHP:

const response = await fetch('/api/user.php', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Nguyen Van A',
    email: 'a@example.com'
  })
});

const data = await response.json();

console.log(data);

Ở phía PHP, dữ liệu JSON có thể được đọc từ request body:

<?php

$input = file_get_contents('php://input');
$data = json_decode($input, true);

$name = $data['name'] ?? '';
$email = $data['email'] ?? '';

$response = [
    'success' => true,
    'name' => $name,
    'email' => $email
];

header('Content-Type: application/json; charset=utf-8');
echo json_encode($response);

Điểm quan trọng là frontend và backend phải thống nhất định dạng dữ liệu. Nếu JavaScript gửi JSON nhưng PHP lại chờ dữ liệu theo kiểu form thông thường, hai phía có thể không nhận được dữ liệu như dự kiến.

Backend trả JSON để frontend xử lý

Một API PHP thường nên trả về response có cấu trúc nhất quán. Chẳng hạn:

<?php

header('Content-Type: application/json; charset=utf-8');

echo json_encode([
    'success' => true,
    'message' => 'Dữ liệu đã được xử lý',
    'data' => [
        'id' => 25
    ]
]);

Frontend có thể kiểm tra kết quả:

const response = await fetch('/api/user.php');
const result = await response.json();

if (result.success) {
  console.log(result.data);
} else {
  console.error(result.message);
}

Thiết kế response thống nhất giúp frontend không phải đoán cấu trúc dữ liệu ở từng endpoint. Đối với hệ thống lớn, đây là yếu tố ảnh hưởng đáng kể đến khả năng bảo trì.

Fetch và CORS: vì sao request có thể bị trình duyệt chặn?

Khi frontend và API nằm trên hai origin khác nhau, trình duyệt áp dụng cơ chế CORS để kiểm soát việc chia sẻ tài nguyên giữa các origin. Đây là nguyên nhân phổ biến khiến một request hoạt động khi gọi trực tiếp từ server nhưng lại gặp lỗi khi gọi từ JavaScript trên trình duyệt.

Ví dụ, website chạy tại một origin nhưng API nằm ở origin khác. Việc server có phản hồi hay không chưa phải yếu tố duy nhất; response còn phải đáp ứng các quy tắc CORS mà trình duyệt yêu cầu.

Đây không phải lỗi riêng của Fetch. Các cơ chế request phía trình duyệt đều chịu ảnh hưởng bởi chính sách bảo mật của trình duyệt.

Preflight request là gì?

Với một số request cross-origin, trình duyệt có thể gửi một request kiểm tra trước, thường được gọi là preflight. Mục đích là xác định server có cho phép request thực tế với phương thức hoặc header mà client muốn sử dụng hay không.

Điều này đặc biệt dễ gặp khi frontend sử dụng các phương thức hoặc header khiến request không thuộc nhóm đơn giản theo quy tắc CORS.

Việc xử lý CORS cần được thực hiện đúng ở phía server. Không nên xem việc thay đổi JavaScript phía client là cách giải quyết mặc định cho vấn đề này.

Những lỗi thường gặp khi sử dụng Fetch

Fetch có cú pháp tương đối ngắn nhưng vẫn có một số lỗi khiến chương trình hoạt động không đúng. Hiểu nguyên nhân sẽ giúp tránh việc sửa lỗi theo kiểu thử ngẫu nhiên.

  • Không kiểm tra response.ok: response 404 hoặc 500 vẫn có thể được trả về dưới dạng Response.
  • Gọi response.json() sai định dạng: nếu body không phải JSON hợp lệ, quá trình phân tích có thể thất bại.
  • Quên JSON.stringify(): khi API yêu cầu JSON, object cần được chuyển sang chuỗi JSON trước khi gửi.
  • Khai báo Content-Type không phù hợp: header phải tương ứng với cách đóng gói body.
  • Không bắt lỗi mạng: request có thể thất bại trước khi nhận được HTTP response.
  • Nhầm CORS với lỗi backend: server có thể hoạt động bình thường nhưng trình duyệt vẫn chặn việc đọc response do chính sách CORS.
  • Giả định mọi response đều có body: một số response thành công có thể không chứa dữ liệu để gọi json().

Kiểm soát request với Request và Response

Với những chức năng đơn giản, gọi trực tiếp fetch(url, options) là đủ. Tuy nhiên, khi ứng dụng có nhiều API hoặc cần tái sử dụng cùng một cấu hình request, việc hiểu rõ hai đối tượng RequestResponse sẽ giúp code được tổ chức tốt hơn.

Tạo Request riêng

Thay vì truyền toàn bộ thông tin trực tiếp vào fetch(), có thể tạo một đối tượng Request trước rồi sử dụng nó:

const request = new Request('/api/products', {
  method: 'GET',
  headers: {
    'Accept': 'application/json'
  }
});

const response = await fetch(request);

if (!response.ok) {
  throw new Error(`HTTP ${response.status}`);
}

const products = await response.json();

console.log(products);

Cách này hữu ích khi request cần được xây dựng trước, kiểm tra hoặc truyền qua nhiều hàm xử lý khác nhau.

Đọc thông tin từ Response

Response không chỉ chứa body. Nó còn cung cấp nhiều thông tin giúp frontend biết server đã phản hồi như thế nào.

const response = await fetch('/api/products');

console.log(response.status);
console.log(response.ok);
console.log(response.statusText);
console.log(response.headers);

Trong đó, status là mã HTTP cụ thể, ok cho biết response có thuộc nhóm thành công hay không, còn headers chứa các header mà server gửi về.

Hủy Fetch và giới hạn thời gian chờ

Một request không nên mặc định được phép chờ vô thời hạn trong các chức năng mà người dùng có thể thao tác liên tục. Fetch hỗ trợ AbortController để hủy request khi không còn cần thiết.

Ví dụ:

const controller = new AbortController();

const request = fetch('/api/products', {
  signal: controller.signal
});

controller.abort();

Sau khi abort(), request tương ứng sẽ bị hủy. Đây là kỹ thuật hữu ích khi người dùng chuyển trang, đóng một chức năng hoặc khi một request mới thay thế request cũ.

Dùng AbortSignal.timeout() để giới hạn thời gian

Trong môi trường hỗ trợ tính năng này, có thể kết hợp signal timeout để tránh chờ quá lâu:

async function loadData() {
  const response = await fetch('/api/products', {
    signal: AbortSignal.timeout(5000)
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return await response.json();
}

Ở ví dụ trên, request được giới hạn trong khoảng 5 giây. Nếu quá thời gian quy định mà request chưa hoàn tất, signal sẽ kích hoạt việc hủy.

Với những ứng dụng cần hỗ trợ nhiều môi trường trình duyệt khác nhau, nên kiểm tra khả năng hỗ trợ hoặc xây dựng cơ chế timeout bằng AbortController theo nhu cầu tương thích của dự án.

Quản lý credentials và cookie khi gọi API

Khi website sử dụng cookie cho phiên đăng nhập, việc gửi request bằng Fetch có thể liên quan đến tùy chọn credentials. Đây là phần cần đặc biệt chú ý khi frontend và backend không nằm cùng origin.

Ví dụ:

const response = await fetch('/api/profile', {
  credentials: 'include'
});

const data = await response.json();

console.log(data);

Tùy chọn này yêu cầu trình duyệt bao gồm credentials phù hợp với request theo chính sách của trình duyệt và server.

Với request cross-origin, việc sử dụng credentials còn liên quan trực tiếp đến cấu hình CORS. Server phải cho phép origin cụ thể và cấu hình response phù hợp. Không nên tùy tiện cho phép mọi origin khi API xử lý dữ liệu riêng tư.

Xây dựng hàm Fetch dùng chung cho website

Nếu một website có vài chục hoặc hàng trăm endpoint, việc viết lặp lại fetch(), kiểm tra status, đọc JSON và xử lý lỗi ở từng nơi sẽ khiến mã nguồn khó bảo trì. Một cách tốt hơn là tạo một hàm trung gian để thống nhất cách gọi API.

Ví dụ:

async function apiFetch(url, options = {}) {
  const response = await fetch(url, {
    ...options,
    headers: {
      'Accept': 'application/json',
      ...(options.headers || {})
    }
  });

  if (!response.ok) {
    throw new Error(`Request failed with status ${response.status}`);
  }

  return response.json();
}

Sau đó, các chức năng khác có thể sử dụng hàm này:

async function loadProducts() {
  try {
    const products = await apiFetch('/api/products');

    console.log(products);
  } catch (error) {
    console.error(error);
  }
}

Lợi ích của cách tổ chức này là các quy tắc chung được đặt ở một nơi. Khi cần thay đổi cách xử lý lỗi, header mặc định hoặc cơ chế xác thực, developer không phải sửa hàng loạt lời gọi Fetch.

Không nên che giấu toàn bộ lỗi

Một hàm API dùng chung không nên bắt mọi lỗi rồi trả về một giá trị như null mà không cung cấp thông tin nguyên nhân. Làm như vậy có thể khiến tầng giao diện không biết request thất bại vì server, dữ liệu không hợp lệ hay lỗi kết nối.

Trong nhiều hệ thống, tốt hơn là để lỗi tiếp tục được truyền lên tầng xử lý phù hợp:

async function apiFetch(url, options = {}) {
  const response = await fetch(url, options);

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  return response.json();
}

async function showProducts() {
  try {
    const products = await apiFetch('/api/products');

    renderProducts(products);
  } catch (error) {
    showError('Không thể tải dữ liệu. Vui lòng thử lại.');
    console.error(error);
  }
}

Như vậy, hàm gọi API chịu trách nhiệm phát hiện lỗi, còn giao diện quyết định cách thông báo lỗi cho người dùng.

Tìm kiếm với Fetch và tránh gửi quá nhiều request

Một tình huống phổ biến là ô tìm kiếm gọi API mỗi khi người dùng nhập một ký tự. Nếu không kiểm soát, người dùng nhập một từ khóa dài có thể tạo ra rất nhiều request liên tiếp.

Ví dụ đơn giản:

const searchInput = document.querySelector('#search');

searchInput.addEventListener('input', async function() {
  const keyword = this.value.trim();

  if (!keyword) {
    return;
  }

  const response = await fetch(
    `/api/search?q=${encodeURIComponent(keyword)}`
  );

  const data = await response.json();

  console.log(data);
});

Cách trên có thể hoạt động nhưng chưa tối ưu. Với chức năng tìm kiếm thực tế, thường nên kết hợp debounce để chờ người dùng ngừng nhập trong một khoảng thời gian ngắn trước khi gửi request.

Ngoài ra, khi request trước chưa hoàn thành mà người dùng tiếp tục nhập, có thể hủy request cũ bằng AbortController để tránh kết quả cũ ghi đè kết quả mới.

let controller = null;
let timer = null;

const searchInput = document.querySelector('#search');

searchInput.addEventListener('input', function() {
  clearTimeout(timer);

  timer = setTimeout(async () => {
    const keyword = this.value.trim();

    if (!keyword) {
      return;
    }

    if (controller) {
      controller.abort();
    }

    controller = new AbortController();

    try {
      const response = await fetch(
        `/api/search?q=${encodeURIComponent(keyword)}`,
        {
          signal: controller.signal
        }
      );

      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }

      const data = await response.json();

      console.log(data);
    } catch (error) {
      if (error.name !== 'AbortError') {
        console.error(error);
      }
    }
  }, 300);
});

Đây là một ví dụ cho thấy Fetch không chỉ được dùng để gửi request đơn lẻ. Khi kết hợp với AbortController và logic giao diện, nó có thể phục vụ các chức năng tương tác phức tạp hơn.

Bảo mật khi sử dụng Fetch

Fetch là công cụ giao tiếp với server, nhưng bản thân việc dùng Fetch không làm ứng dụng tự động an toàn. Các vấn đề như xác thực, phân quyền, kiểm tra dữ liệu đầu vào, CSRF, XSS và quản lý token vẫn phải được thiết kế ở cấp độ hệ thống.

Không tin tưởng dữ liệu nhận từ API

Dữ liệu trả về từ server vẫn cần được xử lý đúng cách trước khi đưa vào DOM. Đặc biệt, không nên lấy một chuỗi không đáng tin cậy rồi gán trực tiếp vào innerHTML nếu không kiểm soát nội dung.

Khi chỉ cần hiển thị văn bản, có thể ưu tiên textContent:

const title = document.querySelector('#title');

title.textContent = data.name;

Cách này giúp nội dung được xem là văn bản thay vì HTML cần trình duyệt phân tích.

HTTPS nên được sử dụng cho dữ liệu quan trọng

Thông tin đăng nhập, dữ liệu cá nhân, token và các dữ liệu quan trọng nên được truyền qua kết nối HTTPS. Fetch không thay thế cho HTTPS; nó chỉ là cơ chế mà JavaScript sử dụng để thực hiện request.

Đối với ứng dụng có tài khoản người dùng, cần đồng thời quan tâm đến cookie, session, token, CORS, CSRF và chính sách bảo mật của server thay vì chỉ tập trung vào đoạn code gọi API.

Fetch khác XMLHttpRequest như thế nào?

XMLHttpRequest là công nghệ lâu đời được sử dụng rộng rãi để thực hiện AJAX trước khi Fetch trở nên phổ biến. Cả hai đều có thể gửi và nhận dữ liệu mà không cần tải lại toàn bộ trang, nhưng cách sử dụng khá khác nhau.

Tiêu chí Fetch XMLHttpRequest
Cú pháp Gọn, phù hợp JavaScript hiện đại Nhiều API và callback hơn
Promise Tích hợp tự nhiên Không dựa trên Promise theo cách nguyên bản
async/await Rất thuận tiện Cần xây dựng lớp Promise riêng nếu muốn sử dụng theo cách tương tự
Đọc response Hỗ trợ json(), text(), blob(), arrayBuffer() và nhiều dạng khác Sử dụng cơ chế response riêng của XMLHttpRequest
Hủy request AbortController Phương thức abort()
Ứng dụng hiện đại Phù hợp với phần lớn API mới Vẫn hữu ích trong một số hệ thống cũ hoặc trường hợp đặc thù

Điểm đáng chú ý là Fetch không phải một phiên bản đổi tên của XMLHttpRequest. Đây là một API có thiết kế khác, đặc biệt phù hợp với mô hình Promise và cách tổ chức JavaScript hiện đại.

Khi nào nên sử dụng Fetch trong dự án?

Fetch phù hợp với phần lớn nhu cầu giao tiếp HTTP từ frontend. Nếu website cần gọi REST API, gửi biểu mẫu, tải dữ liệu động hoặc cập nhật một phần giao diện, Fetch thường là lựa chọn đơn giản và dễ bảo trì.

Đặc biệt, Fetch phù hợp khi:

  • Website sử dụng JavaScript hiện đại.
  • Frontend cần giao tiếp với API JSON.
  • Cần sử dụng async/await để quản lý luồng bất đồng bộ.
  • Cần hủy request bằng AbortController.
  • Cần xây dựng lớp API dùng chung cho nhiều endpoint.
  • Backend cung cấp các endpoint rõ ràng và có cấu trúc response thống nhất.

Ngược lại, nếu dự án cũ đã xây dựng nhiều logic dựa trên XMLHttpRequest hoặc phụ thuộc vào những hành vi đặc thù của công nghệ này, việc thay đổi toàn bộ sang Fetch không nhất thiết phải thực hiện ngay. Quan trọng nhất vẫn là tính ổn định và nhu cầu thực tế của hệ thống.

Những nguyên tắc giúp sử dụng Fetch hiệu quả

Fetch khá dễ bắt đầu nhưng để sử dụng tốt trong một website thực tế, nên hình thành một số nguyên tắc ngay từ đầu.

  1. Luôn hiểu response trước khi đọc body: biết API trả JSON, text hay dữ liệu dạng khác.
  2. Kiểm tra response.ok: đừng xem mọi response nhận được là request thành công.
  3. Thống nhất định dạng API: frontend và backend cần thống nhất request body và response body.
  4. Dùng async/await cho luồng phức tạp: giúp mã nguồn dễ đọc và bảo trì.
  5. Xử lý lỗi ở đúng tầng: không che giấu lỗi khiến việc kiểm tra sự cố trở nên khó khăn.
  6. Hủy request không còn cần thiết: đặc biệt hữu ích với tìm kiếm trực tiếp hoặc các thao tác có request liên tục.
  7. Không đưa dữ liệu không tin cậy trực tiếp vào HTML: luôn cân nhắc vấn đề XSS khi render dữ liệu.
  8. Không đặt bí mật trong mã frontend: mọi dữ liệu đã gửi xuống trình duyệt về bản chất đều có thể bị người dùng quan sát.
  9. Tối ưu số lượng request: debounce, cache hoặc gộp request khi phù hợp có thể cải thiện đáng kể trải nghiệm.
  10. Thiết kế backend và frontend cùng nhau: Fetch chỉ là phương tiện giao tiếp; chất lượng của API mới quyết định phần lớn trải nghiệm phát triển.

Kết luận

Fetch API là một nền tảng quan trọng khi xây dựng website có dữ liệu động. Với một cú pháp tương đối gọn, khả năng làm việc tự nhiên với Promise và async/await, Fetch giúp JavaScript dễ dàng giao tiếp với API mà không phải tải lại toàn bộ trang.

Để sử dụng Fetch đúng cách, không nên chỉ ghi nhớ cú pháp fetch(). Điều quan trọng hơn là hiểu toàn bộ vòng đời của request: chuẩn bị URL và dữ liệu, chọn HTTP method, thiết lập header, gửi body, nhận Response, kiểm tra status, đọc dữ liệu và xử lý lỗi.

Khi dự án phát triển lớn hơn, các kỹ thuật như AbortController, FormData, credentials, debounce và hàm gọi API dùng chung sẽ giúp hệ thống ổn định, dễ bảo trì và phản hồi tốt hơn.

Với website sử dụng backend PHP, Fetch cũng là cầu nối hiệu quả giữa giao diện JavaScript và các endpoint xử lý dữ liệu. Chỉ cần frontend và backend thống nhất rõ phương thức HTTP, định dạng request, response và cơ chế xác thực, bạn có thể xây dựng nhiều chức năng tương tác hiện đại mà không cần tải lại trang sau mỗi thao tác.

  • 0 Bình luận
CEO Bùi Tấn Lực | Founder Web Mới
Bùi Tấn Lực
Tìm hiểu về CEO Bùi Tấn Lực, Founder Web Mới với nhiều năm kinh nghiệm trong lĩnh vực phát triển website, SEO và chia sẻ kiến thức công nghệ
Đánh giá
Chia sẻ nội dung đánh giá của bạn về Fetch API là gì? Hướng dẫn sử dụng Fetch để gửi và nhận dữ liệu
Email, Điện thoại của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *
Đánh giá của bạn
Tên *
Email
Số điện thoại *
Bình luận, Hỏi đáp
Yêu Cầu Báo Giá
Gửi trang web mẫu cần làm theo, chúng tôi sẽ báo giá đến bạn từ Email (tanlucit09@gmail.com - Bùi Tấn Lực) hoặc Zalo (Lực IT - 0398259259) !