Xây dựng trình tạo API Client với Proxy trong JavaScript và TypeScript
Bài viết trình bày cách xây dựng một trình tạo client API tập trung sử dụng Proxy của JavaScript và tính năng overload trong TypeScript, giúp quản lý gọi API trong hệ sinh thái microservices hiệu quả và có kiểu dữ liệu chính xác.

Xây dựng trình tạo API Client với Proxy trong JavaScript và TypeScript
Trong môi trường phát triển microservices, việc cấu hình các instance Axios hoặc Ky rải rác trong dự án thường gây ra tình trạng mã lặp lại, kiểu dữ liệu không nhất quán và khó khăn trong việc đồng bộ cấu hình chung như headers tracking hay chuyển đổi kiểu dữ liệu. Bài viết này giới thiệu cách chuyển đổi từ một cấu hình rối rắm sang một giao diện client API tập trung và sạch sẽ bằng cách sử dụng Proxy trong JavaScript kết hợp với tính năng overload trong TypeScript.
API Client tập trung: Minh họa sử dụng
Thay vì phải import và cấu hình Axios ở từng nơi, bạn chỉ cần khai báo các dịch vụ kèm URL cơ sở và sử dụng trực tiếp client với kiểu dữ liệu tự động:
import { clientGenerator } from "./api-utils";
import { connector } from "./base-connector";
// 1. Định nghĩa và khởi tạo client
const { service1, service2 } = clientGenerator(connector, {
service1: process.env.SERVICE1,
service2: process.env.SERVICE2,
});
// 2. Sử dụng với kiểu dữ liệu tự động
const data = await service1.get<MyResponse>("/user");
// 'data' có kiểu MyResponse trực tiếp
Khi cần lấy toàn bộ response, chỉ cần thêm một flag trong options, kiểu trả về cũng thay đổi tương ứng mà không gặp lỗi kiểu:
const response = await service1.get<MyResponse>("/user", {
resolveWithFullResponse: true,
});
console.log(response.status); // 200
console.log(response.data.ok); // có kiểu rõ ràng
Cách thức hoạt động bên trong: Proxy và overload kiểu
1. Proxy – “Bẫy” mọi phương thức HTTP
Thay vì định nghĩa từng phương thức (get, post, put, ...), một Proxy sẽ “bắt” mọi truy cập thuộc tính và chuyển đổi thành lời gọi conector có chuẩn hóa:
const client = (connector: Connector, baseURL: string) =>
new Proxy({} as Client, {
get(_target, method: HTTPMethod) {
const requestMethod: ClientMethod = <TResponse = unknown>(
endpoint: `/${string}`,
options: ClientRequestConfig = {},
) => {
const requestBase = {
...options,
baseURL,
url: endpoint,
method: method.toUpperCase(),
};
// ép kiểu any để đảm bảo infer kiểu đúng
return connector<TResponse>(requestBase as any);
};
return requestMethod;
},
});
Với cách này, khi connector hỗ trợ thêm method HTTP mới, client cũng tự động tương thích mà không cần chỉnh sửa code hiện tại.
2. Overload kiểu – Thông minh và linh hoạt
Để IDE biết khi nào trả về chỉ data và khi nào trả về toàn bộ response, ta dùng overload hàm trong TypeScript:
type ClientMethod = {
// Trường hợp trả về data
<TResponse = unknown>(
endpoint: `/${string}`,
options?: ClientRequestConfigNormal,
): Promise<TResponse>;
// Trường hợp trả về toàn bộ AxiosResponse
<TResponse = unknown>(
endpoint: `/${string}`,
options: ClientRequestConfigFullResponse,
): Promise<AxiosResponse<TResponse>>;
};
Nhờ overload, ta vừa đảm bảo kiểu an toàn vừa tăng khả năng autocomplete và kiểm tra kiểu tự động trong IDE.
3. Hàm factory clientGenerator
Chức năng chính của clientGenerator là nhận danh sách endpoints và trả về đối tượng client tương ứng:
export const clientGenerator = <T extends ServiceMap, R = {
[K in keyof T]: Client;
}>(
connector: Connector,
services: T,
): R =>
Object.fromEntries(
Object.entries(services).map(([key, baseURL]) => [key, client(connector, baseURL)]),
) as R;
Kết quả là bạn có một tập hợp client APIs sẵn sàng sử dụng theo chuẩn chung.
Ứng dụng thực tiễn với React Server Components và React Query
React Server Components (RSC)
Với RSC, bạn ưu tiên xây dựng các API gọi theo kiểu “một cửa” (One API to rule them all). Việc sử dụng clientGenerator giúp giữ cho component server không bị lẫn lộn với logic cấu hình token, tracking ở backend.
Tích hợp React Query
Bởi client trả về Promise có kiểu rõ ràng, React Query tự động infer kiểu dữ liệu trả về, giảm đáng kể việc phải khai báo kiểu ở từng useQuery:
const { data } = useQuery({
queryKey: ['fines', id],
queryFn: () => fines.get<FinesDetail>(`/fines/${id}`)
});
Điều này đảm bảo dữ liệu nhận được chính xác, phù hợp với kiểu trả về API.
Kết luận
Việc trừu tượng hóa việc tạo client API thông qua một generator tập trung giúp:
- Làm sạch mã nguồn, tránh trùng lặp cấu hình
- Đảm bảo tiêu chuẩn đồng nhất cho mọi microservice (headers, lỗi, tracking)
- Nâng cao trải nghiệm nhà phát triển với kiểu dữ liệu rõ ràng và tự động
- Dễ mở rộng và bảo trì hệ thống
Đây là một khoản đầu tư nhỏ về kiến trúc nhưng có giá trị lợi ích lớn về lâu dài.
Bạn thấy cách tiếp cận với Proxy và overload này thế nào? Bạn đã từng áp dụng Proxy trong utils của mình hay vẫn thích cách định nghĩa rõ ràng, tường minh hơn? Hãy chia sẻ nhé!
Bài viết liên quan

Phần mềm
Anthropic ra mắt Claude Opus 4.7: Nâng cấp mạnh mẽ cho lập trình nhưng vẫn thua Mythos Preview
16 tháng 4, 2026

Công nghệ
Qwen3.6-35B-A3B: Quyền năng Lập trình Agentic, Nay Đã Mở Cửa Cho Tất Cả
16 tháng 4, 2026

Công nghệ
Spotify thắng kiện 322 triệu USD từ nhóm pirate Anna's Archive nhưng đối mặt với bài toán thu hồi
16 tháng 4, 2026
