· trpc / graphql / typescript

tRPC vs GraphQL — khi nào tRPC thắng, khi nào GraphQL thắng

tRPC thắng khi team TypeScript, không có client bên ngoài. GraphQL thắng ngay khi có client không phải TypeScript. Đây là lý do cho từng lựa chọn.

Bởi

1.778 từ · 9 phút đọc

Dùng tRPC nếu team của bạn dùng TypeScript xuyên suốt, client và server nằm cùng repo, và không bao giờ cần expose API đó ra ngoài. Dùng GraphQL ngay khi một trong những điều kiện đó không còn đúng — chỉ cần một client không phải TypeScript là value proposition của tRPC sụp đổ. Phần còn lại bên dưới là nội dung đáng đọc.

Bài này dành cho ai

Developer TypeScript đang chọn API layer cho dự án mới, hoặc đang cân nhắc migration. Bạn đọc đã biết cả hai tool — đây không phải tutorial.

Nếu bạn đang maintain một public API hoặc backend đa ngôn ngữ, nhảy thẳng xuống bảng verdict. tRPC có thể không phù hợp.

Cách chúng tôi so sánh

Phiên bản thử nghiệm: tRPC v11.17.0 (phát hành 2026-04-28), graphql-js v16.14.0, GraphQL Spec September 2025, Pothos stable mới nhất, GraphQL Code Generator mới nhất. Chúng tôi dùng migration của Echobind/Bison làm case thực tế chính (số liệu có thể kiểm chứng từ GitHub PR #282), bổ sung bằng phát biểu của maintainer trong GitHub issues và kinh nghiệm thực tế từ bốn thread Hacker News.

Type safety: gần nhau hơn bạn nghĩ

Pitch của tRPC là type safety không cần codegen. Server export một router type, client nhận nó qua TypeScript generic, và inference tự lo phần còn lại — không schema file, không bước generation, không types bị cũ giữa các lần chạy.

// server/router.ts
export type AppRouter = typeof appRouter;

// client.ts
const client = createTRPCClient<AppRouter>({ links: [...] });
const user = await client.getUser.query('id_bilbo');
// TypeScript biết kiểu của argument và return value từ inference

Giới hạn, theo FAQ chính thức: cần TypeScript ≥ 5.7.2 với "strict": true, và client với server phải trong cùng repo — hoặc server types phải được publish dưới dạng private npm package. Không có ràng buộc đó: “bạn sẽ mất đảm bảo rằng client và server hoạt động cùng nhau.”

GraphQL có hai hướng có type. Pothos (code-first, không codegen) đạt được đảm bảo tương tự qua TypeScript generics và explicit field registration, không overhead runtime nào ngoài graphql-js. Đang dùng trong production tại Airbnb và Netflix. GraphQL Code Generator sinh types từ schema và operation file — nhưng types bị cũ giữa các lần chạy và cần một process graphql-codegen --watch riêng.

Verdict về type safety: cả hai đều có thể đạt đảm bảo compile-time. Pothos loại bỏ codegen. Khoảng cách DX thực sự nằm ở chỗ khác.

Giới hạn field selection — điểm cứng của tRPC

Đây là ràng buộc kiến trúc quan trọng nhất của tRPC.

GraphQL client khai báo chính xác các field cần thiết cho mỗi query. Fragment colocation theo kiểu Relay cho phép mỗi component UI tự quản lý data shape của nó. Ba view của một entity Product có thể request ba tập field khác nhau từ cùng một endpoint.

tRPC không có tương đương. Một maintainer xác nhận trong GitHub discussion #2592 rằng workaround json-mask không giữ được type inference. Các lựa chọn thực tế là over-fetching (một procedure trả về mọi thứ cho tất cả consumer) hoặc procedure sprawl (một procedure riêng cho mỗi data shape).

Với API nhỏ có boundary rõ ràng và ổn định, điều này không phải vấn đề. Với data model lớn, quan hệ phức tạp, nhiều biến thể UI, nó trở thành chi phí thực sự.

Ownership schema và federation đa team

Apollo Federation 2 cho phép nhiều backend team sở hữu các subgraph riêng mà router compose thành một supergraph. Mỗi subgraph có thể dùng ngôn ngữ hoặc framework khác nhau. Client chỉ cần hit một endpoint; router phối hợp phần còn lại.

# Subgraph User của Team A
type User @key(fields: "id") {
  id: ID!
  name: String!
}

# Subgraph Order của Team B
type Order {
  id: ID!
  buyer: User @external
}

tRPC không có tương đương. Import TypeScript types từ một service Go hoặc Python là không thể. Nếu kiến trúc của bạn đã là polyglot microservices, tRPC không còn trong bàn cờ nữa.

Với một service monolith duy nhất, Federation chỉ là overhead không có lợi ích gì. So sánh này chỉ có nghĩa khi bạn thực sự có — hoặc đang xây hướng tới — nhiều service.

Trải nghiệm developer

Điểm tRPC thực sự thắng so với mọi GraphQL toolchain, kể cả Pothos:

Điểm so sánhtRPCGraphQL (hướng Codegen)
AutocompleteTypeScript inference native, luôn cập nhậtCần chạy codegen; stale giữa các lần
”Go To Definition”Nhảy đến source của server procedureNhảy đến file type được generate
Rename refactoringPropagate từ server → client đồng thờiString query field mờ với TypeScript
HTTP status cho lỗiMã chuẩn (400, 401, 403, 500)Luôn HTTP 200; lỗi nằm trong body errors[]
Client không phải TypeScriptChỉ có plugin OpenAPI alphaHỗ trợ đầy đủ không phụ thuộc ngôn ngữ
Schema introspectionKhông có tương đương built-inGraphQL Playground / Introspection API

Vấn đề HTTP 200 là cái bẫy vận hành thực sự. Lỗi GraphQL đến dưới dạng HTTP 200 với một field errors trong response body. Công cụ APM thông thường alert theo 4xx/5xx — lỗi GraphQL vô hình với chúng nếu không có cấu hình tùy chỉnh. Từ thread HN 31285827: “GraphQL servers send 200s unless modified… turned into quite the headache for us.”

Bundle size và hiệu năng

Con số hiệu năng đáng tin cậy nhất trong bài so sánh này đến từ migration Echobind/Bison — họ chuyển starter kit Bison open-source từ Apollo/Nexus/GraphQL Codegen sang tRPC + React Query:

  • Bundle trước: 81.2 KB minified + gzip
  • Bundle sau: 23.7 KB minified + gzip
  • Giảm: 3.5×
  • Lines of code: −1,608 (xóa 3,373, thêm 1,765)

Sự chênh lệch bundle có ý nghĩa trong serverless và edge function, nơi nó ảnh hưởng trực tiếp đến cold start time. Với server-to-server call, nó gần như không đáng kể.

Query latency thuần ở volume thực tế không phải điểm phân biệt có ý nghĩa. Network time và hình dạng database query chiếm phần lớn ở cả hai. Vấn đề N+1 tồn tại trong GraphQL resolver (DataLoader là fix chuẩn) nhưng không ảnh hưởng tRPC — dù procedure của tRPC cũng có thể trả về query kém hiệu quả nếu bạn viết như vậy.

Bài kiểm định quyết định

Một practitioner trên HN diễn đạt chính xác:

“I use GraphQL because my clients are not all TypeScript based… My stack is therefore Rust with GraphQL with clients in React for web and Flutter for mobile.”

Đó là phép thử. Một iOS app, một Flutter client, một Python script đọc API của bạn — type safety của tRPC không apply cho những client đó. Bạn vẫn cần schema ngôn ngữ-trung lập. Nếu phải vừa dùng tRPC (cho TypeScript client) vừa GraphQL hoặc REST (cho mọi người còn lại), bạn đã thêm một layer mà không giảm được độ phức tạp.

Một người có kinh nghiệm GraphQL production sâu (từng làm GraphQL infrastructure tại Twitch) chọn tRPC ở startup của họ: “I still love GraphQL, but man do I not miss all the work maintaining the ‘contract’ between front and back.” Đó là lợi ích DX thực sự so với overhead vận hành thực sự, không phải cảm nhận của người mới.

Mô tả trung thực về niche của tRPC, từ các thread đó: “tRPC is for a single team working on TypeScript frontend to TypeScript backend — that’s niche.” Không phải chỉ trích. Là mô tả chính xác.

Verdict

Chọn tRPC nếu:

  • Team TypeScript duy nhất, client và server trong cùng monorepo
  • Internal tooling: dashboard, admin panel, API nội bộ
  • Không có external API consumer, không bao giờ
  • Team 2–8 người, infrastructure codegen là overhead khó justify
  • Bạn đang dùng T3 stack (tRPC là default)

Chọn GraphQL nếu:

  • Có bất kỳ client không phải TypeScript nào hiện tại hoặc có khả năng: iOS, Android, Flutter, Python, bên thứ ba
  • Backend không phải Node.js/TypeScript (Go, Rust, Python)
  • Developer bên ngoài dùng API của bạn
  • Nhiều frontend team cần query flexibility độc lập
  • Nhiều backend service cần federation
  • Bạn cần schema versioning và field-level deprecation
  • Data model quan hệ phức tạp và component cần các field subset khác nhau của cùng entity

Một tình huống kết thúc so sánh ngay lập tức: một client không phải TypeScript duy nhất. Lúc đó bạn cần REST hoặc GraphQL layer dù sao đi nữa — và nếu phải maintain cả hai, tRPC không còn đơn giản hóa được gì.

Nếu tRPC phù hợp với stack của bạn, quyết định tiếp theo thường là database layer — Prisma vs DrizzleORM TypeScript tốt nhất 2026 đi sâu vào lựa chọn đó.

Lưu ý

Chúng tôi không test plugin alpha @trpc/openapi của tRPC (v11) cho REST/non-TypeScript interop — nó chưa stable vào thời điểm publish và câu chuyện migration có thể thay đổi khi nó trưởng thành.

Con số bundle của Echobind là từ migration năm 2022 của một starter kit cụ thể. Con số của bạn sẽ khác tùy thuộc vào những gì bạn đang dùng trước đó và client graph trông như thế nào.

tRPC v11 (mới nhất: v11.17.0 tính đến 2026-04-28) yêu cầu TypeScript ≥ 5.7.2 và Node.js 18+. React Query v5 là peer dependency — việc đổi tên isLoadingisPending ảnh hưởng đến code v10 hiện có.

Không có reflection “một năm sau” được ghi lại về migration Echobind tính đến ngày publish. Các số liệu lấy từ migration commit.

Tham khảo