← Lập trình JavaScript cơ bản

Bài 10 · Vận dụng · 22 phút

Fetch & JSON: gọi API

Biên soạn bởi Nguyễn Anh Tuấn

Gọi API bằng fetch trong JavaScript: async/await thực chiến, Response & response.json(), bẫy 404 không reject (response.ok), xử lý lỗi mạng & hiển thị dữ liệu.

Mọi app web đều lấy dữ liệu từ server ở xa theo một khuôn cố định: chương trình gửi request (yêu cầu), server trả về response (phản hồi). Request gồm URL - một địa chỉ dữ liệu cụ thể gọi là endpoint - và method: GET xin dữ liệu về, POST gửi dữ liệu lên. Response mang status code (mã trạng thái), vài header mô tả, và body - thường là JSON của bài 5: hồi đó nằm yên trong localStorage, giờ đi qua mạng. Tập endpoint một server mở ra gọi là API; hàm fetch để gọi API có sẵn trong trình duyệt lẫn Node 18+.

giải phẫu một lần hỏi-đáp (URL minh hoạ)

GET https://api.meohamhoc.vn/meo   <- request: method + URL (endpoint)
HTTP/1.1 200 OK                     <- response: status code
Content-Type: application/json      <- header: cho biet body la JSON
[                                   <- body: du lieu JSON
  { "ten": "Mun", "tuoi": 2 },
  { "ten": "Miu", "tuoi": 1 },
  { "ten": "Xam", "tuoi": 3 }
]
  • Request = URL + method (GET lấy về, POST gửi đi); response = status code + header + body.
  • Status code: 2xx ổn · 4xx lỗi phía người gọi (404 = không tìm thấy) · 5xx lỗi phía server.
  • API (Application Programming Interface) trả JSON - định dạng của bài 5; fetch có sẵn trong trình duyệt và Node 18+.

fetch(url) gửi một request GET và trả về Promise<Response> - đúng loại Promise của bài 8 “Bất đồng bộ”, nên dùng async/await là gọn nhất. Điểm bất ngờ: phải await HAI lần, vì fetch resolve ngay khi nhận xong phần đầu response (status + header), còn body vẫn đang về theo dòng - response.json() đọc hết dòng đó mới parse, nên cũng trả về một Promise.

GET với hai lần await - URL minh hoạ (bài tập sẽ dùng API thật); output: chạy thật với server trả đúng dữ liệu này

async function layDanhSachMeo() {
  // await 1: cho server phan hoi
  const response = await fetch("https://api.meohamhoc.vn/meo");
  // await 2: cho doc XONG body roi parse JSON
  const data = await response.json();
  console.log(data); // data la mang that, dung duoc ngay
}
layDanhSachMeo();

Kết quả khi chạy

[
  { ten: 'Mun', tuoi: 2 },
  { ten: 'Miu', tuoi: 1 },
  { ten: 'Xam', tuoi: 3 }
]
  • fetch(url) gửi GET, trả về Promise<Response> - await như mọi Promise (bài 8).
  • await 1 chờ phần đầu (status + header); await 2: response.json() đọc hết body (về theo dòng) rồi parse.
  • Response còn mang status, ok (true khi 200-299), headers.get(...) - Bước 3 dùng ngay.

Nguồn của vô số bug ngoài đời: gọi API, server trả 404, mà chương trình vẫn chạy tiếp như không có gì. Lý do: với fetch, nhận được MỘT response - bất kể status đẹp xấu - đã tính là xong việc, Promise resolve; nó chỉ reject khi lỗi mạng (mất kết nối, sai domain, server không tồn tại…) - không có response nào để trả:

hai loại thất bại - API thật, user 9999 không tồn tại; Chrome báo “Failed to fetch” thay cho “fetch failed”

async function thuHaiLoaiLoi() {
  // 1) tai nguyen khong co that -> server tra 404: fetch van RESOLVE!
  const response = await fetch("https://jsonplaceholder.typicode.com/users/9999");
  console.log("ok:", response.ok, "- status:", response.status);
  // 2) loi MANG that su (sai dia chi, mat ket noi) -> fetch reject
  try {
    await fetch("http://localhost:9/meo"); // khong co server nao o cong 9
  } catch (e) {
    console.log("Loi mang:", e.name, "-", e.message);
  }
}
thuHaiLoaiLoi();

Kết quả khi chạy

ok: false - status: 404
Loi mang: TypeError - fetch failed

Muốn 404/500 cũng được đối xử như lỗi: tự kiểm response.ok, xấu thì throw new Error(...) - đúng bộ throw/try-catch đã luyện ở bài 7 “Xử lý lỗi & debug”. Đây là mẫu chuẩn đáng thuộc lòng:

mẫu chuẩn: kiểm ok, throw, nơi gọi try/catch - output: chạy thật (user 9999 không tồn tại → 404)

async function layDuLieu(url) {
  const response = await fetch(url);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`); // 404/500 -> loi that su
  }
  return response.json();
}
async function main() {
  try {
    const meo = await layDuLieu("https://jsonplaceholder.typicode.com/users/9999");
    console.log(meo);
  } catch (e) {
    console.log("That bai:", e.message);
  }
}
main();

Kết quả khi chạy

That bai: HTTP 404
  • fetch chỉ reject khi LỖI MẠNG (không có response): mất kết nối, sai domain.
  • 404/500 vẫn resolve - phải tự kiểm response.ok (hoặc status) rồi throw new Error.
  • Mẫu chuẩn: layDuLieu kiểm ok + throw; nơi gọi bọc try/catch (bài 7) - mọi thất bại về một chỗ.

Dữ liệu về rồi thì render. Bài 9 “DOM & sự kiện” đã chốt cách an toàn: createElement + đổ chữ bằng textContent - dữ liệu API là dữ liệu bên ngoài, nhét vào innerHTML là mở cửa cho XSS. Mạng thật chậm và hay hỏng - cần “đang tải…” khi chờ, một thông báo dễ hiểu khi lỗi.

render danh sách mèo - chạy trong TRÌNH DUYỆT, dùng lại layDuLieu ở Bước 3

// HTML co san:  <p id="trang-thai"></p>  <ul id="danh-sach"></ul>
const trangThai = document.querySelector("#trang-thai");
const danhSach = document.querySelector("#danh-sach");
async function hienThiMeo() {
  trangThai.textContent = "Dang tai...";
  try {
    const meo = await layDuLieu("https://api.meohamhoc.vn/meo");
    trangThai.textContent = "";
    for (const m of meo) {
      const li = document.createElement("li");
      li.textContent = `${m.ten} - ${m.tuoi} tuoi`; // textContent: an toan
      danhSach.append(li);
    }
  } catch (e) {
    trangThai.textContent = "Khong tai duoc danh sach, thu lai sau nhe.";
  }
}
hienThiMeo();
  • Dữ liệu từ API = dữ liệu bên ngoài → render bằng createElement + textContent (tránh XSS, bài 9).
  • Luôn có trạng thái “đang tải” và thông báo lỗi thân thiện - mạng thật chậm và hay hỏng.
  • Nhờ layDuLieu đã throw đủ hai loại lỗi, giao diện chỉ cần MỘT nhánh catch.

GET là chiều lấy về. Chiều ngược lại - gửi bình luận, thêm một mèo con vào danh sách - dùng POST: vẫn hàm fetch, thêm đối số thứ hai gồm method, headers, body. Body phải là chuỗi - object đi qua JSON.stringify (bài 5) - kèm header Content-Type: application/json để server biết cách đọc:

POST một mèo con mới - URL minh hoạ; output: chạy thật (server trả 201 kèm bản ghi mới)

async function themMeo(meoMoi) {
  const response = await fetch("https://api.meohamhoc.vn/meo", {
    method: "POST",
    headers: { "Content-Type": "application/json" }, // bao: body la JSON
    body: JSON.stringify(meoMoi),                    // object -> chuoi JSON
  });
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}
async function main() {
  console.log("Server tra ve:", await themMeo({ ten: "Bong", tuoi: 1 }));
}
main();

Kết quả khi chạy

Server tra ve: { id: 4, ten: 'Bong', tuoi: 1 }
  • GET lấy dữ liệu về - POST gửi dữ liệu đi; cùng fetch, thêm đối số thứ hai.
  • POST JSON đủ bộ ba: method "POST" + header Content-Type + body JSON.stringify(...).
  • Server thường trả 201 (đã tạo) kèm bản ghi mới - vẫn kiểm response.ok như GET.

Tiếp theo

Mèo con đã gọi được API thật - từ phía gọi. Phía bên kia sợi dây - tự viết server nhận request, trả JSON - là chuyện của khoá Lập trình JavaScript nâng cao (bài “Mạng: HTTP, fetch & API”). Còn bài sau - “Module (ESM) & npm” - chia code thành nhiều file với import/export và dùng thư viện qua npm.

Câu hỏi thường gặp

Không cần cài gì: fetch là hàm có sẵn (global) trong mọi trình duyệt hiện đại, và từ Node 18 trở lên cũng có sẵn (bài 1 đã giới thiệu hai môi trường này) - gọi thẳng trong Console hay file .js. Code cũ hay dùng XMLHttpRequest hoặc thư viện ngoài; chuẩn hiện nay là fetch.

KHÔNG - đây là bẫy nổi tiếng nhất của fetch: nó chỉ reject khi LỖI MẠNG (mất kết nối, sai domain… tức là không nhận được response nào). 404/500 vẫn là một response hợp lệ nên Promise resolve bình thường - muốn coi status xấu là lỗi, bạn phải tự kiểm response.ok rồi throw.

JSON.parse(text) (bài 5) nhận một CHUỖI đã có sẵn trong tay. response.json() làm hai việc một thể: đọc body từ mạng cho hết (bất đồng bộ - vì thế phải await) rồi parse thành object; tương đương JSON.parse(await response.text()). Lưu ý: body của mỗi Response chỉ đọc được MỘT lần - gọi json() lần thứ hai sẽ báo lỗi.

CORS (Cross-Origin Resource Sharing) là cơ chế an toàn của TRÌNH DUYỆT: trang web ở domain A chỉ gọi được API ở domain B khi server B cho phép (qua header Access-Control-Allow-Origin). Bị chặn thì fetch reject kèm lỗi CORS trong Console - dù server vẫn sống; code chạy bằng Node không bị ràng buộc này. API luyện tập công khai như JSONPlaceholder mở CORS nên mèo con cứ yên tâm thử.

Tick những điều em tự tin làm được. Càng lên cao, em càng hiểu sâu.

Tick những điều em tự tin làm được sau khi học bài này. 0/6

Trả lời vài câu để chắc rằng em đã nắm bài.

Câu 1/3 Điểm: 0

Gọi await fetch(url) mà server trả về 404 Not Found. Chuyện gì xảy ra?

  1. 1

    GET đầu tiên

    Trong Console trình duyệt (hoặc Node), fetch https://jsonplaceholder.typicode.com/users với hai lần await, rồi in ra name của từng user.

    Hoàn thành khi: Mảng có 10 user, in ra 10 cái tên - user đầu tiên là “Leanne Graham”.

  2. 2

    Soi Response

    Với cùng URL trên, in response.status, response.okresponse.headers.get("content-type").

    Hoàn thành khi: 200, true, và content-type chứa application/json.

  3. 3

    Tự gây 404

    fetch địa chỉ https://jsonplaceholder.typicode.com/users/9999. Có lỗi nào bị ném ra không? In response.okresponse.status.

    Hoàn thành khi: KHÔNG có exception - fetch vẫn resolve; okfalse, status404.

  4. 4

    Lỗi mạng thật

    fetch một địa chỉ không tồn tại (vd https://khong-ton-tai.meohamhoc.vn) trong try/catch, in lỗi bắt được. Khác gì bài tập 3?

    Hoàn thành khi: Lần này RƠI VÀO catch với TypeError - vì không có response nào (trình duyệt báo “Failed to fetch”, Node báo “fetch failed”).

  5. 5

    layDuLieu chuẩn

    Viết hàm layDuLieu(url) kiểm response.ok, xấu thì throw new Error("HTTP " + response.status); thử với URL đúng và URL 9999 ở bài tập 3.

    Hoàn thành khi: URL đúng trả về dữ liệu; URL 9999 nhảy vào catch, in “HTTP 404”.

  6. 6

    Render lên trang

    Trong một file HTML: lấy https://jsonplaceholder.typicode.com/todos?_limit=5, hiển thị 5 todo bằng createElement + textContent, có “Đang tải…” lúc chờ và thông báo khi lỗi.

    Hoàn thành khi: 5 dòng todo hiện ra (todo đầu: “delectus aut autem”); sửa URL cho sai thì thấy thông báo lỗi thân thiện thay vì trang trắng.