Chuyển đổi từ Apollo sang tRPC: Cách chúng tôi giảm 89% lỗi và tăng tốc độ xử lý API
Bài viết này chia sẻ hành trình chuyển đổi từ Apollo Federation sang tRPC, giúp giảm 89% số lượng lỗi và cải thiện 67% thời gian phản hồi. Tác giả cũng phân tích kiến trúc sản xuất thực tế, những sai lầm gặp phải và lợi ích của an toàn kiểu dữ liệu (type safety) từ đầu đến cuối trong hệ thống xử lý 2,4 triệu yêu cầu mỗi ngày.

Chuyển đổi từ Apollo sang tRPC: Cách chúng tôi giảm 89% lỗi và tăng tốc độ xử lý API
Sáu tháng trước, tôi từng là một người ủng hộ nhiệt thành cho GraphQL Federation. Chúng tôi đã dành sáu tháng để xây dựng một biểu đồ liên kết (federated graph) với Apollo, hoàn chỉnh với việc ghép schema (schema stitching), cấu hình gateway và pipeline CI/CD phức tạp để tái tạo kiểu dữ liệu trên mỗi lần commit. Trên giấy tờ, nó trông rất đẹp. Nhưng trong môi trường sản xuất thực tế? Nó là một thảm họa chực chờ xảy ra mỗi khi triển khai.
Điểm gãy đến vào một buổi chiều thứ Sáu khi triển khai thường lệ. Đội sản phẩm đã cập nhật kiểu dữ liệu của một trường trong một dịch vụ, schema được tái tạo thành công, các bài kiểm tra通过了, và chúng tôi đã đẩy nó lên. Ba mươi phút sau, ứng dụng di động bắt đầu bị crash vì client iOS vẫn đang sử dụng các kiểu dữ liệu cũ được tạo ra từ hai giờ trước đó. Schema đã được phiên bản hóa. Gateway đã được cập nhật. Nhưng quá trình codegen phía client chưa chạy vì ai đó đã quên kích hoạt nó. Đó là một điểm đau kinh điển của GraphQL Federation.
Đó là lúc tôi bắt đầu nghiên cứu tRPC. Điều thu hút tôi là lời hứa về an toàn kiểu dữ liệu từ đầu đến cuối (end-to-end type safety) mà không cần các nghi thức schema phức tạp. Không có file SDL. Không có bước codegen. Không có gateway liên kết. Chỉ có TypeScript, từ đầu đến cuối. Mặc dù hoài nghi, nhưng sau khi thấy các chỉ số sản xuất từ các công ty chạy tRPC ở quy mô lớn, tôi đã thuyết phục đội ngũ của mình xây dựng một khái niệm chứng minh (proof of concept).
Dưới đây là câu chuyện hoàn chỉnh về quá trình di chuyển của chúng tôi, bao gồm cả những sai lầm, những chiến thắng về hiệu suất không ngờ tới, và cái nhìn về kiến trúc sản xuất mà chúng tôi đang vận hành hôm nay để xử lý 2,4 triệu yêu cầu mỗi ngày với thời gian hoạt động 99,97%.
Thực tế kỹ thuật: tRPC thực sự mang lại gì?
An toàn kiểu dữ liệu mà không gánh nặng Schema
Đây là điều mà ít người nói với bạn về GraphQL Federation: schema trở thành điểm thất bại duy nhất (single point of failure). Với tRPC, các kiểu TypeScript của bạn chính là hợp đồng. Không có đại diện trung gian. Không có SDL để duy trì. Không có registry schema để giữ đồng bộ giữa các môi trường.
tRPC đạt được an toàn từ đầu đến cuối mà không cần định nghĩa schema
Khi chúng tôi chạy Apollo Federation, một thay đổi kiểu dữ liệu điển hình trông như thế này: Cập nhật schema GraphQL -> Chạy codegen -> Commit các file đã tạo -> Cập nhật triển khai resolver -> Cập nhật truy vấn client -> Chạy codegen client -> Triển khai cả hai dịch vụ -> Hy vọng không có gì bị hỏng.
Với tRPC thì sao? Cập nhật interface TypeScript -> Xong. Client biết ngay lập tức vì họ đang chia sẻ cùng một định nghĩa kiểu.
Hiệu suất thực sự quan trọng
Chúng tôi đã chạy các bài kiểm tra tải sản xuất so sánh thiết lập Apollo Federation cũ với việc triển khai tRPC mới. Các con số thật đáng ngạc nhiên. Hiệu suất cold start — rất quan trọng đối với các hàm serverless của chúng tôi — được cải thiện 75%. Chi phí overhead của gateway Apollo Federation thêm 180ms cho cold start. tRPC chỉ mất 45ms. Đó là con số trước khi chúng tôi chạm đến logic kinh doanh thực tế.
Thời gian phản hồi trung bình dưới tải liên tục giảm từ 38ms xuống 12ms. Nhưng điều thực sự quan trọng là độ trễ P95 và P99. Với Apollo, P95 của chúng tôi ở mức 85ms và P99 là 156ms. Sau khi di chuyển, P95 giảm xuống 28ms và P99 xuống 42ms. Những độ trễ đuôi này giết chết trải nghiệm người dùng, đặc biệt là trên mạng di động.
Đo lường dưới tải liên tục 10.000 yêu cầu/phút
Câu chuyện về kích thước bundle cũng kịch tính không kém. Thiết lập Apollo Client của chúng tôi với hỗ trợ Federation nặng 142KB khi nén (gzipped). tRPC với React Query chỉ 28KB. Nhẹ hơn 80%. Trên các kết nối chậm hơn, điều này dịch sang thời gian tải trang ban đầu nhanh hơn 2-3 giây. Người dùng thực sự nhận thấy sự khác biệt ngay lập tức.
Kiến trúc sản xuất: Cách chúng tôi thực sự xây dựng hệ thống này
Thiết lập Monorepo hoạt động hiệu quả
Thiết lập sản xuất của chúng tôi chạy trên một monorepo workspace pnpm, sử dụng Next.js 14 App Router cho frontend và tRPC cho tất cả giao tiếp API. Chúng tôi có 12 microservices, mỗi dịch vụ exposing router tRPC riêng của nó, và một lớp gateway hợp nhất tất cả lại.
12 microservices, 2,4M yêu cầu/ngày, 99,97% thời gian hoạt động
Mỗi dịch vụ sở hữu logic miền và cơ sở dữ liệu của riêng mình. Dịch vụ người dùng nói chuyện với PostgreSQL, dịch vụ sản phẩm sử dụng MongoDB cho dữ liệu danh mục, và dịch vụ đơn hàng tận dụng Redis để quản lý phiên. Vẻ đẹp của tRPC ở đây là an toàn kiểu dữ liệu chảy qua toàn bộ stack. Khi dịch vụ sản phẩm thay đổi kiểu trường, TypeScript sẽ ngay lập tức gắn cờ mọi người tiêu dùng.
Chiến lược Request Batching và Caching
Một mối quan tâm mà mọi người thường nêu ra về tRPC là thiếu tính năng request batching tích hợp sẵn như GraphQL cung cấp. Thực tế là: batching của React Query đủ cho hơn 90% trường hợp sử dụng, và thực tế nó đơn giản hơn để gỡ lỗi so với các mẫu DataLoader của GraphQL. Chúng tôi đang chạy 10.000 yêu cầu mỗi phút qua môi trường sản xuất và batching hoạt động hoàn hảo.
Lớp caching của chúng tôi kết hợp Redis cho dữ liệu chia sẻ và cache thông minh của React Query ở phía client. Sự kết hợp này mạnh mẽ vì React Query biết chính xác dữ liệu nó có và có thể phục vụ từ cache ngay lập tức, trong khi cache Redis phía máy chủ xử lý dữ liệu chéo người dùng một cách hiệu quả. Chúng tôi đang thấy tỷ lệ cache hit 87% trên dữ liệu sản phẩm và 92% trên sở thích người dùng.
Quy trình di chuyển: Cách chúng tôi thực sự thực hiện
Giai đoạn 1: Mẫu Strangler Fig
Chúng tôi không thực hiện một cuộc viết lại toàn diện (big-bang rewrite). Đó là cách bạn kết thúc với sáu tháng giá trị kinh doanh bằng không và một đội ngũ điều hành hoảng loạn. Thay vào đó, chúng tôi sử dụng mẫu Strangler Fig: chạy cả hai hệ thống song song, di chuyển các điểm cuối (endpoint) từng cái một, chứng minh sự ổn định, rồi chuyển sang cái tiếp theo.
Chúng tôi bắt đầu với các điểm cuối chỉ đọc (read-only) có lưu lượng truy cập cao nhưng rủi ro kinh doanh thấp, chẳng hạn như tra cứu hồ sơ người dùng, truy vấn danh mục sản phẩm, v.v. Những cái này mang lại cho chúng tôi dữ liệu sản xuất thực tế về hiệu suất và độ tin cậy mà không gây rủi ro cho các thao tác ghi quan trọng. Chúng tôi đã chạy song song cả hai API trong ba tuần, so sánh tỷ lệ lỗi và chỉ số độ trễ trước khi chuyển đổi hoàn toàn.
Giai đoạn 2: Các thay đổi quan trọng (Mutations)
Khi chúng tôi đã tự tin vào các thao tác đọc, chúng tôi giải quyết các mutations, bao gồm tạo đơn hàng, xử lý thanh toán, cập nhật tồn kho — những thứ thực sự tốn tiền nếu bị hỏng. Đây là nơi an toàn kiểu dữ liệu của tRPC thực sự tỏa sáng. Với GraphQL, chúng tôi liên tục phải đối phó với các trường có thể null, các đối số tùy chọn và sự trôi dạt schema. Với tRPC, nếu các kiểu biên dịch, bạn đã loại bỏ một loại lỗi hợp đồng API hoàn toàn.
Chúng tôi tìm thấy chính xác hai lỗi runtime trong quá trình di chuyển endpoint mutation. Cả hai đều liên quan đến pooling kết nối cơ sở dữ liệu, không phải tRPC itself. Đáng chú ý: đây là một cuộc di chuyển, không phải xây dựng xanh (greenfield), nên logic kinh doanh đã được chứng minh.
Triển khai thực tế: Mã nguồn thực sự được triển khai
Thiết lập Router phía máy chủ
Dưới đây là thiết lập router sản xuất thực tế của chúng tôi, loại bỏ logic kinh doanh nhưng cho thấy các mẫu thực tế chúng tôi sử dụng. Điều này xử lý xác thực, xác thực yêu cầu, xử lý lỗi và hợp nhất kiểu trên các microservices:
// apps/api/src/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { Context } from "./context";
import superjson from "superjson";
const t = initTRPC.context<Context>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
export const router = t.router;
export const publicProcedure = t.procedure;
// Middleware xác thực
const isAuthed = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({ ctx: { session: ctx.session, userId: ctx.session.user.id } });
});
export const protectedProcedure = t.procedure.use(isAuthed);
Thiết lập Client với Next.js 14
Thiết lập Next.js của chúng tôi sử dụng App Router mới với React Server Components nếu có thể. Dưới đây là cấu hình client thực tế mà chúng tôi đang chạy trong sản xuất:
// apps/web/src/trpc/client.ts
import { createTRPCReact } from "@trpc/react-query";
import { httpBatchLink } from "@trpc/client";
import type { AppRouter } from "@/server/routers/_app";
import superjson from "superjson";
export const trpc = createTRPCReact<AppRouter>();
export function createTRPCClient() {
return trpc.createClient({
links: [
httpBatchLink({
url: process.env.NEXT_PUBLIC_API_URL + "/api/trpc",
transformer: superjson,
headers: async () => {
const session = await getSession();
return {
authorization: session?.token ? `Bearer ${session.token}` : "",
};
},
}),
],
});
}
Bài học kinh nghiệm: Những sai lầm và chiến thắng trung thực
Sai lầm chúng tôi đã mắc phải
Sai lầm lớn đầu tiên: cố gắng sao chép field-level batching của GraphQL. Chúng tôi đã dành hai tuần để xây dựng một hệ thống batching tùy chỉnh trước khi nhận ra batching tích hợp sẵn của React Query là hoàn toàn đầy đủ. Chúng tôi đã xóa 800 dòng mã và hiệu suất thực sự được cải thiện vì cách tiếp cận đơn giản hơn có ít chi phí overhead hơn.
Sai lầm thứ hai: over-validating trên client. Chúng tôi đang chạy xác thực Zod trên cả client và server, nghĩ rằng nó sẽ bắt lỗi sớm hơn. Điều thực sự xảy ra là chúng tôi đã tạo ra sự không nhất quán giữa xác thực client và server dẫn đến các trạng thái lỗi gây nhầm lẫn. Bây giờ chúng tôi chỉ xác thực một lần trên server. Client tin tưởng các kiểu TypeScript.
Sai lầm thứ ba: không thiết lập giám sát đúng cách sớm đủ. tRPC nhanh đến mức chúng tôi không nhận thấy các suy giảm hiệu suất cho đến khi chúng trở nên đáng kể. Bây giờ chúng tôi có Datadog APM trên mọi thủ tục, theo dõi độ trễ P50, P95, P99 và tỷ lệ lỗi.
Chiến thắng bất ngờ
Chiến thắng bất ngờ lớn nhất là tốc độ của nhà phát triển (developer velocity). Đội ngũ của chúng tôi hiện vận hành tính năng nhanh hơn 40% vì họ không phải chuyển đổi ngữ cảnh giữa SDL, codegen và triển khai. Bạn viết thủ tục của mình, TypeScript lan truyền các kiểu, và bạn xong việc.
Chiến thắng thứ hai là việc onboarding nhanh hơn cho các nhà phát triển mới. Với GraphQL Federation, các kỹ sư mới cần một tuần để hiểu schema, gateway và pipeline codegen trước khi họ có thể đóng góp. Với tRPC, họ đang đẩy code vào ngày thứ hai. Nếu bạn biết TypeScript và Next.js, bạn biết API của chúng tôi.
Khi nào KHÔNG nên sử dụng tRPC
Hãy rõ ràng về điều này: tRPC không phải là viên đạn bạc. Nếu bạn đang xây dựng một API công khai mà các bên thứ ba sẽ tiêu thụ, GraphQL hoặc REST có ý nghĩa hơn. Bạn cần tài liệu schema, phiên bản hóa và truy cập không phụ thuộc ngôn ngữ. tRPC chỉ dành cho TypeScript.
Nếu bạn có ứng dụng di động được xây dựng bằng Swift hoặc Kotlin, tRPC sẽ không giúp ích gì cho bạn. Nó tuyệt vời cho các ứng dụng web nơi bạn kiểm soát cả client và server, nhưng nó không giải quyết an toàn kiểu dữ liệu đa nền tảng như protobuf hoặc GraphQL có thể.
Và một cách trung thực, nếu thiết lập GraphQL của bạn hoạt động tốt và bạn không gặp các điểm đau mà chúng tôi đã gặp, thì không có lý do gì để di chuyển. Chúng tôi di chuyển vì Federation đang tốn chi phí thời gian của nhà phát triển và sự ổn định sản xuất. Nếu đó không phải là tình huống của bạn, hãy tiếp tục sử dụng những gì đang hoạt động.
Kết luận
Chắc chắn là có. Không chút do dự. Quá trình di chuyển đã mất sáu tuần nỗ lực kỹ thuật tập trung, và chúng tôi đã thu lại khoản đầu tư đó gấp mười lần nhờ giảm việc sửa lỗi, phát triển tính năng nhanh hơn và cải thiện trải nghiệm nhà phát triển. Đội ngũ của chúng tôi vận hành tính năng nhanh hơn 40%, người dùng của chúng tôi có hiệu suất tốt hơn, và chúng tôi ngủ ngon hơn vào ban đêm biết rằng một loại vấn đề runtime toàn bộ đã được loại bỏ.
Nhưng đây là thực tế: tRPC sẽ không giải quyết các vấn đề tổ chức. Nếu đội ngũ của bạn gặp khó khăn với GraphQL do quy trình kém hoặc quyền sở hữu không rõ ràng, chuyển sang tRPC sẽ không magically sửa chữa điều đó. Những gì tRPC sửa chữa là toàn bộ loại vấn đề liên quan đến đồng bộ hóa schema, tạo kiểu và sự trôi dạt hợp đồng API.
Nếu bạn đang chạy một monorepo TypeScript và gặp đau đớn với sự phức tạp của GraphQL Federation, hãy cân nhắc nghiêm túc tRPC. Bắt đầu nhỏ — di chuyển một dịch vụ, đo lường kết quả và mở rộng từ đó. Đó là những gì chúng tôi đã làm, và nó đã thay đổi cơ bản cách chúng tôi xây dựng API.
Bài viết liên quan
Công nghệ
Giải quyết bài toán "con gà và quả trứng" cho các sàn giao dịch hai chiều
20 tháng 4, 2026

Phần mềm
Microsoft tung bản vá khẩn cấp khắc phục lỗi khởi động lại liên tục trên Windows Server
20 tháng 4, 2026
Công nghệ
Cách đây 10 năm, ai đó đã viết một bài kiểm tra cho Servo với ngày hết hạn là năm 2026
19 tháng 4, 2026
