· 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 Ethan
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ánh | tRPC | GraphQL (hướng Codegen) |
|---|---|---|
| Autocomplete | TypeScript inference native, luôn cập nhật | Cần chạy codegen; stale giữa các lần |
| ”Go To Definition” | Nhảy đến source của server procedure | Nhảy đến file type được generate |
| Rename refactoring | Propagate từ server → client đồng thời | String query field mờ với TypeScript |
| HTTP status cho lỗi | Mã chuẩn (400, 401, 403, 500) | Luôn HTTP 200; lỗi nằm trong body errors[] |
| Client không phải TypeScript | Chỉ có plugin OpenAPI alpha | Hỗ trợ đầy đủ không phụ thuộc ngôn ngữ |
| Schema introspection | Không có tương đương built-in | GraphQL 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 Drizzle và ORM 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 isLoading → isPending ả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.
Related reading
- Deno vs Node.js — cuộc tranh luận đã có hồi kết?
- Prisma vs Drizzle 2026 — So sánh chi tiết TypeScript ORM
- Go vs TypeScript — lựa chọn ngôn ngữ backend cho năm 2026
Tham khảo
- tRPC FAQ — yêu cầu monorepo
- Hướng dẫn migrate tRPC v11
- Thông báo tRPC v11
- tRPC GitHub discussion #2592 — field selection
- tRPC GitHub releases
- graphql-js releases
- Pothos schema builder
- GraphQL Code Generator
- Apollo Federation 2 docs
- Echobind: Why we ditched GraphQL for tRPC
- Echobind/Bison GitHub PR #282 — số liệu kiểm chứng
- HN thread: TRPC — end-to-end typesafe APIs
- HN thread: Why use GraphQL? There is tRPC now
- HN thread: GraphQL Is a Trap?
- HN thread: People ditching GraphQL for tRPC