Cách Mình Tích Hợp APIs

Các pattern để tích hợp với dịch vụ bên ngoài một cách đáng tin cậy và dễ bảo trì

Dịch vụ bên ngoài nằm ngoài tầm kiểm soát. Nó sập bất tử, thay đổi không báo trước, rate limit bất ngờ, trả về data lung tung. Tích hợp API tốt không phải "cầu cho nó đừng lỗi" mà là "lỗi thì mình xử lý sao cho user không bị ảnh hưởng".

Ý tưởng cốt lõi: API bên ngoài sẽ fail. Câu hỏi không phải "có fail không" mà là "fail thì mình bảo vệ user và app bằng cách nào".

Nguyên Tắc Tích Hợp

1. Gói API vào một chỗ

Đừng gọi API bên ngoài trực tiếp từ business logic. Tạo một lớp client wrapper -- mọi thứ liên quan tới API nằm gọn ở đây: auth, format request, parse response, xử lý lỗi.

┌─────────────────────────────────────────────────────────────┐
│                     ỨNG DỤNG CỦA MÌNH                        │
│  Routes, components, business logic                         │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                      API CLIENT                              │
│  Wrapper duy nhất: auth, request, response, error           │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│                    DỊCH VỤ BÊN NGOÀI                         │
│  API bên thứ ba                                              │
└─────────────────────────────────────────────────────────────┘

Lợi ích rõ ràng: đổi provider thì sửa một file. Test business logic thì mock client là xong. Mọi quirk đặc thù của API xử lý tại một điểm duy nhất chứ không lan khắp codebase.

2. Transform data về domain của mình

API trả về user_id, app mình dùng userId. API trả về nested object 5 tầng, mình chỉ cần 3 fields. Đừng để cấu trúc data của người khác chi phối kiến trúc app mình.

Transform ngay tại lớp client:

  1. Nhận response
  2. Validate shape
  3. Map thành domain model của mình
  4. Từ đó trở đi, app chỉ nói ngôn ngữ của mình

Họ đổi field name? Mình sửa đúng 1 chỗ trong client. Không phải lục 50 file.

3. Mọi request đều có thể fail

Gọi API bên ngoài không giống gọi hàm local. Mạng chậm, server sập, response sai format -- chuyện bình thường. Mỗi lời gọi cần ba thứ:

  • Timeout: 30 giây không trả lời thì cut, đừng chờ mãi
  • Error handling: Phân loại lỗi -- retry được hay không
  • Fallback: API sập thì user thấy gì? Cached data? Thông báo? Hay cả app chết theo?

Phân loại lỗi để biết xử lý sao:

  • Retry được: lỗi mạng, rate limit (429), server lỗi (5xx) -- thử lại
  • Không retry: bad request (400), lỗi auth (401/403), not found (404) -- sửa input hoặc config
  • Vĩnh viễn: resource bị xóa, access bị thu hồi -- xử lý graceful, báo user

4. Lưu raw response

Hôm nay mình chỉ cần 5 fields từ response. Sáu tháng sau, product yêu cầu thêm 2 fields mà mình đã vứt. Data cũ thì fetch lại sao được?

Lưu raw response cùng với parsed data. Parse lại thì lúc nào cũng được, còn raw data mất là mất.

5. Tôn trọng rate limit

Bắn 10.000 request cùng lúc rồi bị block, feature hỏng mấy tiếng -- chuyện này xảy ra thường hơn mình tưởng. Trước khi code, đọc docs xem limit bao nhiêu rồi implement throttling vào client luôn.

Rate limit tồn tại vì provider không chịu nổi tải vô hạn. Mình bị limit thì mình là vấn đề, không phải họ.

Khi Nào Thì Làm Gì?

Authentication

KiểuDùng khi
API Key trong headerProvider yêu cầu, chỉ gọi server-side, đơn giản stateless
OAuthHành động thay mặt user, cần phân quyền theo scope
Signed requestBảo mật cao, webhook verification, provider yêu cầu ký

API key không bao giờ để lộ ra client. Luôn gọi từ server.

Retry hay không?

Retry khi:

  • Network timeout, lỗi kết nối
  • 429 có retry-after header
  • 5xx (lỗi tạm thời phía server)

Không retry khi:

  • 400 -- input sai, retry cũng sai
  • 401/403 -- auth sai, retry vô ích
  • 404 -- resource không tồn tại

Retry thế nào: Exponential backoff -- chờ 1s, 2s, 4s. Thêm jitter (random nhẹ) tránh thundering herd. Tối đa 3-5 lần rồi cho fail luôn.

Cache hay không?

Cache khi: data ít thay đổi, request chậm hoặc tốn kém, rate limit chặt, data cũ một chút vẫn chấp nhận được.

Không cache khi: phải real-time, mỗi request unique, hoặc cache phức tạp hơn giá trị nó mang lại.

Invalidate: theo thời gian (hết hạn sau N phút), theo event (có thay đổi thì clear), hoặc manual (cho user force refresh).

Lỗi một phần trong batch

Xử lý 1000 items, 950 thành công, 50 fail. Giờ sao?

Fail cả batch nếu cần all-or-nothing -- thành công một phần tạo trạng thái lỗ mãng hơn là fail sạch.

Xử lý được bao nhiêu hay bấy nhiêu nếu các item độc lập -- track từng item, trả về chi tiết thành công/thất bại, để caller quyết định bước tiếp.

API sập thì sao?

Bốn lựa chọn, tùy context:

  1. Dùng cached data: "Đây là data 5 phút trước, có thể chưa mới nhất"
  2. Ẩn feature: Phần phụ thuộc API thì tạm disable
  3. Queue lại: Nhận request, xử lý khi API phục hồi
  4. Chuyển provider: Có backup service thì switch sang

Quyết định này phải làm lúc dev, không phải lúc đang cháy nhà.

Những Lỗi Hay Gặp

Không đặt timeout

Request treo vô hạn, thread pool cạn dần, mọi thứ chậm rì rì rồi chết. Luôn đặt timeout -- mặc định 10-30 giây tùy operation.

Phớt lờ rate limit

Bắn request liên tục cho tới khi nhận 429, feature ngừng hoạt động mấy tiếng. Đọc docs, implement throttling, batch request với delay.

Tin tưởng data bên ngoài

API "hứa" trả về object với 5 fields, nhưng thực tế có khi thiếu, có khi null, có khi format khác. Validate response trước khi dùng, xử lý field thiếu, đừng assume.

Để API lộ khắp codebase

Tên provider xuất hiện trong 20 file, business logic xử lý error code đặc thù API, domain model copy nguyên cấu trúc bên ngoài. Wrap lại, transform tại boundary, throw error riêng của mình.

Test bằng API thật

Test fail random vì API sập. Chạy chậm vì gọi mạng. Tốn tiền vì API tính phí. Mock client trong test, test client riêng với recorded response.

Không ghi log gì

Hai tuần sau có bug: "API trả về gì?" -- "Không biết." Debug bằng đoán =)). Lưu raw request + response, log tại lớp client, kèm timestamp và request ID.

Checklist

Ổn nếu:

  • API client tách biệt (một file/module)
  • Data bên ngoài transform thành domain model
  • Mọi lời gọi có timeout
  • Lỗi được phân loại và xử lý
  • Rate limit được respect
  • Raw response được lưu để debug

Cần sửa nếu:

  • Chi tiết API nằm trong business logic
  • Có lời gọi không timeout
  • Hay bị rate limit
  • Không tái tạo được lỗi vì không có log
  • Test gọi API thật
  • Một API sập kéo cả app sập theo

Pattern Nâng Cao

Circuit Breaker

API liên tục fail mà mình cứ gọi hoài thì lãng phí. Circuit breaker tự động "ngắt mạch":

  1. Theo dõi lỗi gần đây
  2. Lỗi vượt ngưỡng → mở circuit → fail ngay, không gọi nữa
  3. Sau timeout → cho phép 1 request thử
  4. Thành công → đóng circuit, hoạt động bình thường
  5. Vẫn fail → giữ mở, chờ tiếp

Giống cầu dao điện trong nhà -- chập điện thì ngắt để bảo vệ, không phải để cản.

Health Check

Đừng đợi user báo "API không hoạt động". Chủ động kiểm tra:

  • Kết nối được không?
  • Auth còn valid không?
  • Response time chấp nhận được không?
  • Data trả về có hợp lệ không?

Check khi khởi động app, chạy nền định kỳ, và trước các thao tác quan trọng.

Timeout Strategy

  • Connection timeout (5-10s): chờ thiết lập kết nối
  • Read timeout (tùy operation): chờ nhận response
  • Overall deadline: tổng thời gian cho cả operation, kể cả retry

Fail nhanh rồi retry tốt hơn treo mãi một chỗ.

Xử Lý Batch Dài

Khi process nhiều items:

  1. Track tiến trình: bao nhiêu xong, bao nhiêu còn
  2. Báo user: hiển thị progress, đừng để user đoán
  3. Cho phép hủy: user cần dừng thì dừng được
  4. Lưu checkpoint: bị gián đoạn thì không mất phần đã xong

Xử lý theo batch, update progress sau mỗi batch, lưu checkpoint để resume được.