Frontend không “gọi một URL” đơn giản. Nó thực hiện một HTTP request, chờ server xử lý, kiểm tra response, parse dữ liệu và quyết định cách cập nhật UI khi request thành công hoặc thất bại.
Một lớp gọi API tốt cần giải quyết ít nhất bốn vấn đề:
- gửi request đúng method, header và body;
- phân biệt lỗi mạng với lỗi HTTP;
- parse response theo đúng định dạng;
- hủy hoặc bỏ qua request cũ khi người dùng tạo ra request mới.
Hai lựa chọn phổ biến trong frontend là Fetch API và Axios.
1. Fetch API
Fetch API là API native của các trình duyệt hiện đại, vì vậy không cần cài thêm package. Với các trình duyệt rất cũ, có thể cần polyfill.
Fetch trả về một Promise<Response>. Response.body là một ReadableStream, nhưng trong phần lớn ứng dụng ta không đọc stream trực tiếp mà dùng các method như response.json() hoặc response.text() để parse body.
1.1. Điểm dễ sai: fetch không reject khi HTTP trả lỗi
fetch chỉ reject khi request thất bại ở tầng mạng, chẳng hạn mất kết nối hoặc bị chặn bởi CORS. Các status như 400, 401, 404 và 500 vẫn resolve thành một Response bình thường.
Vì vậy, luôn kiểm tra response.ok hoặc response.status trước khi coi request là thành công.
1 | async function requestJSON(url, options = {}) { |
Một abstraction như trên giúp toàn bộ application có cùng một quy ước xử lý response và error, thay vì mỗi component tự viết một phiên bản khác nhau.
1.2. GET với async/await
1 | async function getCats() { |
Không nên nuốt lỗi bằng cách luôn trả về [] trong tầng request. Làm vậy khiến UI không phân biệt được hai trạng thái: server thực sự trả về danh sách rỗng và request đã thất bại.
1.3. POST JSON
Khi gửi một object JSON, cần serialize body bằng JSON.stringify và khai báo Content-Type.
1 | async function createCat(attributes) { |
Content-Type mô tả định dạng của request body. Nó không thay thế cho việc serialize object thành chuỗi JSON.
1.4. Hủy request với AbortController
Khi người dùng search liên tục hoặc rời khỏi một màn hình, request cũ có thể hoàn thành sau request mới và ghi đè dữ liệu mới nhất. AbortController giúp hủy request không còn cần thiết.
1 | const controller = new AbortController(); |
Trong ứng dụng thực tế, controller thường được tạo theo từng lần search hoặc trong lifecycle của component.
2. Axios
Axios là một HTTP client chạy được trong browser và Node.js. So với Fetch, Axios cung cấp sẵn một số tiện ích như:
- tự động serialize object JSON trong request;
- tự động parse JSON response trong các trường hợp phổ biến;
- reject promise khi status nằm ngoài nhóm 2xx theo mặc định;
- timeout và cancellation;
- interceptor để xử lý authentication, logging hoặc chuẩn hóa error.
Cài đặt:
1 | npm install axios |
2.1. Tạo một Axios instance
Không nên lặp lại baseURL, timeout và header ở từng request. Tạo một instance làm API boundary cho application.
1 | import axios from 'axios'; |
Nếu API không dùng wrapper { data: ... }, hãy trả về data trực tiếp. Quan trọng nhất là chọn một contract rõ ràng và giữ nó nhất quán.
2.2. Xử lý lỗi Axios
Axios phân biệt được response lỗi từ server, request đã gửi nhưng không nhận được response và lỗi xảy ra khi cấu hình request.
1 | import axios from 'axios'; |
Không nên hiển thị nguyên văn error.message cho người dùng. Tầng UI nên chuyển lỗi kỹ thuật thành thông báo có ý nghĩa, còn log chi tiết nên được gửi tới hệ thống monitoring.
3. Những vấn đề production cần quan tâm
3.1. Loading, success và error là ba trạng thái khác nhau
Một request không chỉ có hai trạng thái “có dữ liệu” và “không có dữ liệu”. UI nên phân biệt ít nhất:
idle: chưa bắt đầu request;loading: đang chờ response;success: nhận dữ liệu hợp lệ;error: request thất bại.
Nếu không mô hình hóa các trạng thái này, UI rất dễ hiển thị dữ liệu cũ như thể đó là dữ liệu mới hoặc hiển thị màn hình rỗng khi request chỉ đang loading.
3.2. CORS và authentication
CORS là chính sách của browser, không phải lỗi riêng của Fetch hay Axios. Server phải cho phép đúng origin, method và header mà frontend sử dụng.
Không đưa secret key vào frontend vì mọi giá trị gửi tới browser đều có thể bị người dùng xem. Với cookie-based authentication, cần cấu hình cả phía client và server, ví dụ credentials: 'include' với Fetch, cùng chính sách cookie và CSRF phù hợp.
3.3. Retry không phải lúc nào cũng an toàn
Có thể retry một số request đọc dữ liệu khi lỗi tạm thời. Không nên tự động retry mọi POST, vì request có thể đã được server xử lý nhưng response bị mất; retry có thể tạo bản ghi trùng.
Nếu cần retry một thao tác ghi, API nên hỗ trợ idempotency key hoặc một cơ chế deduplication tương đương.
3.4. Đừng để component biết quá nhiều về HTTP
Component nên gọi một hàm nghiệp vụ như getCats() thay vì tự biết endpoint, header, cách parse response và format error. API layer là nơi phù hợp để tập trung:
- endpoint và HTTP method;
- authentication header;
- schema của response;
- mapping error;
- logging và retry policy.
Cách tổ chức này giúp thay đổi backend hoặc đổi từ Fetch sang Axios mà không kéo theo việc sửa toàn bộ UI.
4. Chọn Fetch hay Axios?
| Tiêu chí | Fetch | Axios |
|---|---|---|
| Dependency | Native trên browser hiện đại | Cần cài package |
| HTTP error | Tự kiểm tra response.ok |
Reject ngoài nhóm 2xx theo mặc định |
| JSON | Tự gọi response.json() |
Tự parse trong trường hợp phổ biến |
| Timeout | Tự kết hợp với AbortController |
Có option timeout |
| Interceptor | Tự xây dựng | Có sẵn |
| Phù hợp | Ứng dụng nhỏ, muốn ít dependency | Ứng dụng lớn, cần API layer nhất quán |
Không có lựa chọn tuyệt đối tốt hơn. Fetch đủ tốt nếu application có một abstraction rõ ràng. Axios đáng cân nhắc khi team cần một HTTP client có sẵn timeout, interceptor và error model thống nhất.
Kết luận
Điểm quan trọng không phải là chọn Fetch hay Axios, mà là xây dựng một boundary đáng tin cậy giữa frontend và backend.
Một implementation tốt phải kiểm tra status HTTP, tôn trọng contract của API, xử lý lỗi minh bạch, hủy request cũ khi cần và không đưa secret vào browser. Khi những nguyên tắc này được đóng gói ở một API layer, phần còn lại của UI có thể tập trung vào state và trải nghiệm người dùng thay vì lặp lại các chi tiết HTTP.