Menu
ESC

Nhập từ khóa để tìm kiếm

↑↓ Di chuyển
Enter Mở
ESC Đóng

Đang tải...

Bài 37 — API Design — REST vs gRPC vs GraphQL

Architecture Decision-Making Bài 37/60

Mở đầu — vì sao bài này quan trọng

Khi bạn thiết kế một hệ thống, có một quyết định tưởng nhỏ nhưng ảnh hưởng lan tỏa suốt vòng đời sản phẩm: API sẽ giao tiếp theo phong cách nào? REST, gRPC hay GraphQL? Đây không phải câu hỏi về "công nghệ nào ngầu hơn", mà là câu hỏi về hợp đồng (contract) giữa các đội, giữa client và server, giữa hệ thống của bạn và đối tác bên ngoài.

Chọn sai không làm hệ thống sập ngay lập tức. Nhưng nó âm thầm sinh ra chi phí: mobile app tải chậm vì phải gọi 7 endpoint để render một màn hình; đội frontend và backend cãi nhau mỗi sprint vì payload không đúng ý; hoặc bạn ép gRPC vào một API công khai để rồi không đối tác nào tích hợp nổi. Là người ra quyết định kiến trúc, bạn cần hiểu bản chất của từng phong cách để chọn đúng ngữ cảnh, chứ không chọn theo trend.

Trong bài này, chúng ta sẽ mổ xẻ ba phong cách API phổ biến nhất hiện nay, so sánh chúng trên những trục quan trọng (hiệu năng, hợp đồng, trải nghiệm client, khả năng cache), và quan trọng nhất — xây dựng một khung quyết định để bạn biết khi nào chọn cái nào.

Khái niệm cốt lõi

REST — Resource-oriented, dựa trên HTTP

REST (Representational State Transfer) mô hình hóa hệ thống thành tài nguyên (resource) và dùng chính ngữ nghĩa của HTTP để thao tác lên chúng.

  • Resource-oriented: URL biểu diễn tài nguyên, ví dụ /users/123/orders nghĩa là "các đơn hàng của user 123".
  • HTTP verbs: GET để đọc, POST để tạo, PUT/PATCH để cập nhật, DELETE để xóa. Ngữ nghĩa nằm sẵn trong giao thức.
  • JSON qua HTTP/1.1 hoặc HTTP/2: định dạng dữ liệu phổ biến nhất, con người đọc được, mọi ngôn ngữ đều parse được.
  • Cache-friendly: vì GET là idempotent và có ngữ nghĩa rõ ràng, bạn tận dụng được cache của HTTP (header Cache-Control, ETag), CDN, proxy... gần như miễn phí.
  • Dễ khám phá (browsable): chỉ cần curl hoặc trình duyệt là gọi được, không cần công cụ đặc biệt.
Điểm mạnh của REST là tính phổ quát và độ trưởng thành của hệ sinh thái. Điểm yếu là hai vấn đề kinh điển: over-fetching (server trả nhiều field hơn client cần) và under-fetching (client phải gọi nhiều endpoint để ghép đủ dữ liệu cho một màn hình).

gRPC — Contract-first, hiệu năng cao

gRPC là framework RPC (Remote Procedure Call) của Google, xây trên HTTP/2 và dùng Protocol Buffers (protobuf) làm định dạng dữ liệu.

  • Contract-first: bạn định nghĩa service và message trong file .proto, rồi sinh code client/server tự động cho hàng chục ngôn ngữ. Hợp đồng là nguồn chân lý duy nhất.
  • Binary, nhỏ gọn: protobuf mã hóa nhị phân, payload nhỏ hơn JSON đáng kể, parse nhanh hơn.
  • HTTP/2 multiplexing: nhiều request chạy song song trên một kết nối; hỗ trợ streaming hai chiều (client stream, server stream, bidirectional).
  • Type-safe mạnh mẽ: sai kiểu dữ liệu bị bắt lỗi ngay lúc compile, không phải lúc runtime.
Điểm mạnh của gRPC là hiệu năng và độ chặt chẽ — lý tưởng cho giao tiếp service-to-service nội bộ, độ trễ thấp, throughput cao. Điểm yếu: trình duyệt không hỗ trợ gRPC trực tiếp (phải qua gRPC-Web + proxy), khó debug bằng mắt thường (binary), và không thân thiện với người tích hợp bên ngoài quen với REST.

GraphQL — Client tự khai báo dữ liệu cần

GraphQL là một ngôn ngữ truy vấn cho API, do Facebook tạo ra để giải quyết đúng nỗi đau over/under-fetching của REST trên mobile.

  • Client quyết định shape dữ liệu: client gửi một query mô tả chính xác các field mình cần, server trả về đúng như vậy — không thừa, không thiếu.
  • Một endpoint duy nhất: thường là /graphql, mọi thứ đi qua POST với body là query.
  • Schema mạnh (typed): toàn bộ API có một schema thống nhất, tự mô tả, hỗ trợ introspection (client tự khám phá được API).
  • Gộp nhiều tài nguyên trong một request: lấy user + orders + reviews trong một round-trip.
Điểm mạnh: cực kỳ linh hoạt cho client, giảm số round-trip, tuyệt vời khi có nhiều loại client khác nhau (iOS, Android, web) với nhu cầu dữ liệu khác nhau. Điểm yếu: cache phức tạp (không dùng được HTTP cache đơn giản vì mọi thứ là POST), nguy cơ N+1 query phía server, và rủi ro query "độc" làm server quá tải nếu không giới hạn độ sâu/độ phức tạp.

Bảng so sánh nhanh

TrụcRESTgRPCGraphQL
Định dạngJSON (text)Protobuf (binary)JSON (text)
Giao thứcHTTP/1.1, HTTP/2HTTP/2HTTP (thường /graphql)
Hợp đồngLỏng (OpenAPI tùy chọn)Chặt (.proto)Chặt (schema)
HTTP cacheRất tốtKhôngKém
Over/under-fetchCó vấn đềÍt (do design kỹ)Giải quyết tốt
StreamingHạn chếNative, 2 chiềuSubscriptions
Trình duyệtNativeCần gRPC-WebNative
Debug bằng mắtDễKhóTrung bình
Kịch bản lý tưởngAPI công khai, CRUDNội bộ, low-latencyMobile, nhiều client

Tình huống thực tế

Ví dụ 1 — Sàn TMĐT Việt Nam chuyển màn hình sản phẩm từ REST sang GraphQL

Một sàn thương mại điện tử tầm trung ở TP.HCM (giả định, đặt tên là ShopViet) có app mobile render trang chi tiết sản phẩm. Với REST, mỗi lần mở trang, app phải gọi: GET /products/{id} (thông tin sản phẩm), GET /products/{id}/reviews (đánh giá), GET /sellers/{id} (thông tin shop), GET /products/{id}/related (sản phẩm liên quan) và GET /promotions?product={id} (khuyến mãi). Năm request tuần tự vì một số phụ thuộc nhau, tổng thời gian tải trung bình khoảng 1,8 giây trên mạng 4G ở khu vực ngoại thành.

Đội mobile chuyển màn hình này sang GraphQL. Giờ đây một query duy nhất khai báo cần: tên sản phẩm, giá, 5 review mới nhất, tên + rating của shop, 4 sản phẩm liên quan, và khuyến mãi đang chạy. Backend resolve tất cả trong một round-trip. Thời gian tải giảm còn khoảng 0,9 giây, dung lượng dữ liệu truyền giảm 40% vì không còn field thừa.

Bài học: GraphQL tỏa sáng khi client cần ghép nhiều tài nguyên và bạn muốn kiểm soát chính xác payload. Nhưng ShopViet không vứt bỏ REST — họ giữ REST cho các webhook thanh toán và API đối tác, chỉ dùng GraphQL cho tầng mobile. Đúng công cụ, đúng chỗ.

Ví dụ 2 — Fintech dùng gRPC cho giao tiếp nội bộ giữa microservices

Một công ty fintech ở Singapore vận hành hệ thống thanh toán với hàng chục microservice: payment-service, fraud-detection, ledger, notification... Ban đầu các service gọi nhau qua REST/JSON. Ở peak giờ trưa, mỗi giao dịch kích hoạt chuỗi 6-7 lời gọi nội bộ, và chi phí serialize/deserialize JSON cộng với overhead HTTP/1.1 làm P99 latency của luồng thanh toán chạm 400ms — vượt ngưỡng SLA nội bộ 250ms.

Họ chuyển các lời gọi service-to-service sang gRPC. Payload protobuf nhỏ hơn khoảng 60% so với JSON tương đương, HTTP/2 multiplexing loại bỏ head-of-line blocking, và fraud-detection tận dụng server-streaming để trả kết quả từng phần. P99 giảm còn 180ms. Thêm vào đó, hợp đồng .proto được version hóa trong một repo chung khiến việc thay đổi schema giữa các đội trở nên tường minh — không còn cảnh "sao field này biến mất" giữa hai team.

Bài học: gRPC là lựa chọn mạnh cho giao tiếp nội bộ, độ trễ thấp, giữa các service mà bạn kiểm soát cả hai đầu. Đáng chú ý: các API công khai của fintech này (cho merchant tích hợp) vẫn là REST, vì merchant không muốn học protobuf.

Ví dụ 3 — Startup ép GraphQL vào một API công khai và trả giá

Một startup SaaS về quản lý kho (giả định, tên WareFlow) quyết định "hiện đại hóa" bằng cách phơi API công khai chỉ bằng GraphQL cho khách hàng tích hợp. Ý định tốt: linh hoạt, một endpoint. Nhưng thực tế phát sinh:

  • Khách hàng doanh nghiệp lớn có sẵn hệ thống ETL quen với REST + HTTP cache, giờ không cache được vì mọi query là POST /graphql.
  • Một khách hàng viết query lồng sâu (orders → items → product → supplier → orders...) vô tình tạo query cực nặng, làm database CPU vọt 90% trong giờ cao điểm.
  • Đội support tốn nhiều thời gian hướng dẫn khách viết query đúng, thay vì chỉ đưa link tài liệu endpoint như REST.
Sau ba tháng, WareFlow bổ sung một tầng REST đơn giản cho các thao tác phổ biến nhất, đặt giới hạn độ sâu và query complexity cho GraphQL, và giữ GraphQL cho các khách hàng kỹ thuật cao thực sự cần.

Bài học: Sự linh hoạt của GraphQL đẩy gánh nặng sang phía client và phía vận hành server. Với API công khai cho đối tác đa dạng, REST vẫn thường là mẫu số chung an toàn nhất. Đừng chọn phong cách chỉ vì nó "mới".

Hướng dẫn từng bước

Đây là quy trình để chọn phong cách API cho một context cụ thể:

  • Xác định ai là người tiêu thụ API (consumer). Nội bộ (service của chính bạn) hay bên ngoài (đối tác, khách hàng public)? Đây là yếu tố quyết định lớn nhất. Consumer nội bộ → gRPC là ứng viên mạnh. Consumer public đa dạng → REST an toàn.
  • Đánh giá đặc tính client. Có mobile app cần tiết kiệm băng thông và giảm round-trip không? Có nhiều loại client với nhu cầu dữ liệu khác nhau không? Nếu có → GraphQL đáng cân nhắc. Nếu chủ yếu là CRUD đơn giản → REST.
  • Cân nhắc yêu cầu hiệu năng. Đặt ra ngân sách latency và throughput (liên hệ với bài NFR/Performance). Nếu bạn cần P99 rất thấp và call nội bộ dày đặc → gRPC. Nếu tải vừa phải → REST/GraphQL đều ổn.
  • Kiểm tra nhu cầu cache. Dữ liệu đọc nhiều, ít đổi, muốn tận dụng CDN/proxy? → REST cho bạn cache HTTP gần như miễn phí. GraphQL và gRPC khó cache ở tầng HTTP hơn nhiều.
  • Xét nhu cầu streaming/real-time. Cần stream hai chiều, đẩy dữ liệu liên tục? → gRPC streaming hoặc GraphQL subscriptions. REST thuần thì hạn chế (phải dùng SSE/WebSocket riêng).
  • Đánh giá độ trưởng thành đội ngũ và hệ sinh thái. Đội đã quen công cụ nào? Có sẵn tooling (gateway, monitoring, codegen) không? Một công nghệ "tối ưu về lý thuyết" nhưng đội không vận hành nổi là một lựa chọn tồi.
  • Chấp nhận kiến trúc lai (hybrid). Trong thực tế, hệ thống trưởng thành thường dùng cả ba: gRPC cho internal, REST cho public/partner, GraphQL cho tầng BFF (Backend-for-Frontend) phục vụ mobile/web. Đừng ép một phong cách duy nhất cho mọi ngữ cảnh.
  • Ghi lại quyết định. Viết một ADR (liên hệ Bài 4-5) nêu rõ context, các lựa chọn đã cân nhắc, và lý do chọn — để 6 tháng sau người kế nhiệm hiểu vì sao.

Lỗi thường gặp & mẹo

Lỗi 1 — Chọn theo trend thay vì theo context. "gRPC nhanh nên ta dùng gRPC cho mọi thứ" là ngụy biện. gRPC nhanh cho internal, nhưng phơi ra public thì đối tác chật vật. Luôn hỏi: consumer là ai, ràng buộc thật là gì?

Lỗi 2 — Bỏ quên cache khi chọn GraphQL/gRPC. Nhiều đội chuyển sang GraphQL rồi ngạc nhiên vì tải server tăng — vì mất khả năng cache HTTP đơn giản. Mẹo: nếu dữ liệu đọc-nhiều-ghi-ít, cân nhắc kỹ chi phí mất cache, hoặc bổ sung persisted queries + cache tầng ứng dụng.

Lỗi 3 — Không giới hạn query complexity trong GraphQL. Một query lồng sâu có thể hạ gục database. Mẹo: đặt giới hạn độ sâu (depth limit), query complexity scoring, và rate limit (liên hệ Bài 40) ngay từ đầu cho API public.

Lỗi 4 — Coi REST là "không có hợp đồng". REST dễ trở thành mớ endpoint tùy hứng. Mẹo: dùng OpenAPI/Swagger để định nghĩa hợp đồng, sinh client tự động, và có tài liệu sống. REST vẫn có thể contract-first.

Lỗi 5 — Dùng verb HTTP sai ngữ nghĩa. GET /deleteUser?id=5 phá vỡ toàn bộ ưu điểm của REST (cache, idempotency, an toàn). Mẹo: GET chỉ đọc và không side-effect; DELETE mới để xóa. Tôn trọng ngữ nghĩa HTTP.

Mẹo bonus — N+1 trong GraphQL. Nếu bạn dùng GraphQL, hãy áp dụng DataLoader (batching + caching) ngay từ đầu để tránh mỗi field con lại bắn một query database riêng.

Bài tập thực hành

  • Phân loại consumer. Liệt kê tất cả API trong một hệ thống bạn từng làm. Với mỗi API, đánh dấu consumer là internal hay external, và nhu cầu (CRUD, streaming, mobile...). Sau đó gán phong cách phù hợp và giải thích một câu.
  • Đo over-fetching. Chọn một màn hình app bạn quen. Ghi ra tất cả endpoint REST nó gọi và tổng số field trả về. Ước tính bao nhiêu field thực sự được hiển thị. Tỷ lệ lãng phí là bao nhiêu? GraphQL có giúp được không?
  • Viết ADR mini. Cho tình huống: "Xây API cho một ứng dụng đặt xe với app mobile (iOS/Android) + dashboard web nội bộ cho vận hành + webhook cho đối tác thanh toán." Chọn phong cách cho từng phần và viết một ADR ngắn (context, options, decision, consequences).
  • Thiết kế phòng thủ GraphQL. Giả sử bạn phải mở GraphQL public. Liệt kê 4 biện pháp bảo vệ server khỏi query độc hại và giải thích mỗi cái ngăn được rủi ro gì.

Tóm tắt

Không có phong cách API "tốt nhất" — chỉ có phong cách phù hợp nhất với context. Ba lựa chọn chính:

  • REST: resource-oriented, dựa trên ngữ nghĩa HTTP, cache tốt, dễ khám phá, phổ quát. Mặc định an toàn cho API công khai và CRUD. Điểm yếu: over/under-fetching.
  • gRPC: contract-first với protobuf trên HTTP/2, binary nhỏ gọn, streaming mạnh, type-safe. Lý tưởng cho giao tiếp nội bộ service-to-service, low-latency. Điểm yếu: không thân thiện trình duyệt và người tích hợp ngoài.
  • GraphQL: client tự khai báo dữ liệu cần, một endpoint, giải quyết over/under-fetching. Tuyệt cho mobile và nhiều loại client. Điểm yếu: cache khó, rủi ro query nặng, N+1.
Người ra quyết định kiến trúc giỏi không hỏi "cái nào ngầu", mà hỏi: consumer là ai, ràng buộc hiệu năng và cache thế nào, đội có vận hành nổi không? Và câu trả lời thực tế thường là kiến trúc lai — gRPC bên trong, REST cho đối tác, GraphQL cho tầng client — chứ không phải một phong cách duy nhất cho mọi ngữ cảnh. Đừng quên ghi lại lựa chọn của bạn bằng một ADR để quyết định hôm nay còn có ý nghĩa cho người kế nhiệm ngày mai.