Xây dựng trình tạo API Client với Proxy trong JavaScript và TypeScript

07 tháng 4, 2026·4 phút đọc

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

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 được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗