Chọn phong cách API không phải là cuộc thi giữa ba cú pháp có thể thay thế lẫn nhau. REST trên HTTP, GraphQL và gRPC đưa ra những contract khác nhau, đặt độ phức tạp ở những vị trí khác nhau và phù hợp với những đường đi traffic khác nhau. Một API công khai cho đối tác, lớp tổng hợp dữ liệu cho màn hình mobile và luồng service-to-service có latency thấp có thể cùng nằm trong một hệ thống nhưng cần ba kiểu giao tiếp khác nhau.
Câu hỏi hữu ích vì thế không phải “Công nghệ nào tốt nhất?” mà là “Tập hợp đánh đổi nào chấp nhận được tại boundary này?” Hãy bắt đầu từ consumer và failure mode, sau đó đánh giá semantics, giới hạn phân phối, vận hành và quyền sở hữu. Kết quả có thể là một protocol duy nhất, nhưng mixed architecture thường mới là câu trả lời nhất quán.
Bắt đầu từ contract, không phải transport
Cả ba hướng đều có thể tạo contract chặt chẽ, nhưng mỗi hướng xem một khái niệm khác nhau là trung tâm.
| Phong cách | Trọng tâm contract | Đơn vị tự nhiên | Nguồn sự thật thường dùng | Rủi ro thiết kế chính |
|---|---|---|---|---|
| REST | Resource và HTTP semantics | Representation tại một URI | OpenAPI cùng server implementation | Biến mọi operation thành một lệnh POST tùy ý |
| GraphQL | Đồ thị ứng dụng có type và có thể duyệt | Các field do client chọn | GraphQL schema và resolver | Mở đồ thị tiện dụng nhưng không đặt giới hạn chi phí |
| gRPC | Remote method và message có type | Unary hoặc streaming RPC | File .proto |
Để client phụ thuộc quá sát vào method nội bộ dễ thay đổi |
REST contract được thiết kế tốt sẽ dùng đúng semantics sẵn có của HTTP. Read an toàn dùng GET; tạo resource thường dùng POST; thay toàn bộ và cập nhật một phần dùng PUT và PATCH; xóa dùng DELETE. Status code, conditional request, media type và cache header đều là contract chứ không phải phần trang trí. OpenAPI mô tả contract đó để làm tài liệu, validation và sinh code:
openapi: 3.1.0
info:
title: Orders API
version: 1.0.0
paths:
/orders/{orderId}:
get:
operationId: getOrder
parameters:
- name: orderId
in: path
required: true
schema: { type: string }
responses:
"200":
description: Order found
headers:
ETag:
schema: { type: string }
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
description: Order does not exist
components:
schemas:
Order:
type: object
required: [id, status, total]
properties:
id: { type: string }
status: { type: string, enum: [pending, paid, shipped] }
total: { type: number, minimum: 0 }
GraphQL đặt type graph mà client có thể chọn field vào trung tâm. Schema dưới đây cho phép client yêu cầu một order và chỉ lấy những field cần thiết. Nullability có ý nghĩa chính xác: Order! nói rằng giá trị order đã trả về không thể là null, còn order(id: ID!): Order vẫn cho phép trường hợp “không tìm thấy” được biểu diễn bằng null.
type Query {
order(id: ID!): Order
}
type Order {
id: ID!
status: OrderStatus!
total: Money!
items(first: Int = 20, after: String): OrderItemConnection!
}
type OrderItemConnection {
nodes: [OrderItem!]!
endCursor: String
hasNextPage: Boolean!
}
type OrderItem {
productId: ID!
quantity: Int!
}
type Money {
amount: String!
currency: String!
}
enum OrderStatus {
PENDING
PAID
SHIPPED
}
Connection không phải chi tiết phụ. Pagination, authorization, nullability và chi phí của field phải được thiết kế ngay trong graph. GraphQL loại bỏ việc tạo quá nhiều endpoint, không loại bỏ độ phức tạp của domain.
Protocol Buffers mô tả message và RPC method độc lập với một ngôn ngữ lập trình cụ thể. Số hiệu field là wire contract và phải giữ ổn định:
syntax = "proto3";
package commerce.v1;
message GetOrderRequest {
string order_id = 1;
}
message Order {
string id = 1;
string status = 2;
int64 total_minor_units = 3;
string currency = 4;
}
service OrdersService {
rpc GetOrder(GetOrderRequest) returns (Order);
rpc WatchOrders(WatchOrdersRequest) returns (stream Order);
}
message WatchOrdersRequest {
repeated string order_ids = 1;
}
Contract này hỗ trợ generated client và binary message gọn. Tuy nhiên, RPC không vì thế mà trở thành lời gọi hàm local: deadline, cancellation, retry, partial failure và backward compatibility vẫn đi qua network boundary.
Tip
Hãy đánh giá chất lượng contract độc lập với lựa chọn protocol. Một tài liệu OpenAPI rõ ràng có thể chặt chẽ hơn GraphQL schema thiếu kỷ luật; một GraphQL schema được tiến hóa cẩn thận có thể an toàn hơn các RPC method phản chiếu implementation thường xuyên thay đổi.
Ghép protocol với topology và client
Đường đi giữa consumer và owner thường có sức nặng lớn hơn cú pháp payload. Browser giao tiếp HTTP thông thường rất tốt, nhưng JavaScript trong browser nhìn chung không được thao tác trực tiếp HTTP/2 frame. Native gRPC vì vậy cần proxy tương thích như Envoy cùng gRPC-Web, hoặc một JSON/GraphQL gateway. Hop bổ sung này ảnh hưởng đến streaming mode, header, cách chuyển đổi lỗi, deployment và debugging.
REST thường là lựa chọn ít ma sát nhất cho public API, webhook, browser client đơn giản, file transfer và integration cần tận dụng curl, proxy, CDN cùng HTTP tooling chuẩn. Resource model cũng tạo boundary tổ chức ổn định: consumer phụ thuộc vào representation của order, không phụ thuộc vào tập hợp service call nội bộ hiện tại.
GraphQL mạnh nhất khi một bề mặt sản phẩm cần ghép dữ liệu từ nhiều domain và các client view khác nhau đáng kể. Đội mobile có thể yêu cầu shape nhỏ hơn dashboard desktop mà không phải xin backend tạo endpoint mới. Sự linh hoạt đó chỉ có giá trị khi graph có ownership, batching, pagination, authorization và demand control rõ ràng. Nếu thiếu chúng, gateway trở thành distributed join engine khó quan sát.
gRPC phù hợp với mạng service-to-service được kiểm soát, generated client đa ngôn ngữ, request rate cao và stream sống lâu. Unary RPC rất hợp với command và query có type. Server streaming, client streaming và bidirectional streaming là thành phần first-class của protocol, dù vẫn bị giới hạn bởi hỗ trợ của ngôn ngữ và proxy. Với các fact bất đồng bộ cần replay, fan-out hoặc lưu giữ bền vững, event broker thường đúng abstraction hơn việc giữ RPC stream mở vô thời hạn.
So sánh hành vi dưới tải production
Performance phụ thuộc workload. Protobuf payload nhỏ thường encode và truyền nhanh hơn JSON tương đương, còn HTTP/2 multiplexing giảm nhiều chi phí connection. Tuy nhiên, database query, cross-region hop hoặc N+1 resolver có thể áp đảo chi phí serialization hàng bậc độ lớn. Hãy benchmark toàn bộ representative call ở concurrency thực tế; đừng chọn protocol chỉ từ microbenchmark kích thước payload.
| Mối quan tâm | REST | GraphQL | gRPC |
|---|---|---|---|
| Caching | HTTP cache tự nhiên theo URI, method, Cache-Control, ETag, Vary |
Thường dùng normalized cache phía app/client; persisted query qua GET có thể dùng CDN |
Thường cache trong application; HTTP shared cache không hiểu RPC |
| Error | HTTP status cộng body ổn định như Problem Details | HTTP có thể là 200 với data và errors; có thể trả partial data |
Canonical status code, message và typed status detail |
| Streaming | Chunked response, SSE, WebSocket hoặc cơ chế HTTP mới; semantics khác nhau | Subscription thường chạy qua WebSocket hoặc SSE; incremental delivery tùy hỗ trợ | Unary, client, server và bidirectional streaming là native |
| Payload | Thường là JSON, dễ đọc nhưng dài | Thường là JSON, shape theo selection set | Protobuf gọn và bị ràng buộc bởi schema |
| Browser | Native | Native qua HTTP | Cần gRPC-Web, Connect hoặc gateway |
| Kiểm soát tải | Rate limit theo route, identity và method | Cần giới hạn depth, breadth, complexity, timeout và pagination | Cần deadline, message limit, concurrency limit và flow control |
Caching cho thấy khác biệt semantics rất rõ. GET /products/42 có cache key ổn định mà browser, reverse proxy và CDN đều hiểu. GraphQL thường gửi nhiều operation vào một endpoint, nên shared cache cần persisted operation identifier, biến đã canonicalize hoặc resolver-level cache. Normalized cache phía client có thể rất mạnh nhưng cần object identity ổn định toàn cục và invalidation mutation cẩn thận. gRPC thường dựa vào local cache hoặc caching layer chuyên dụng vì HTTP cache thông thường không hiểu message của method.
Error model cũng định hình hành vi client. REST nên ánh xạ trung thực protocol outcome và có thể dùng RFC 9457 Problem Details cho failure mà máy đọc được. GraphQL tách transport failure khỏi execution result, đồng thời có thể trả dữ liệu dùng được cùng field error. Client không được xem mọi HTTP 200 là thành công. gRPC có canonical status như INVALID_ARGUMENT, NOT_FOUND, UNAVAILABLE; retry policy còn phải xét idempotency, deadline và chỉ dẫn của server.
Warning
Automatic retry chỉ an toàn khi operation là idempotent hoặc có idempotency key. Retry một payment mutation, REST POST hay gRPC command đã timeout mà không deduplicate có thể áp dụng side effect hai lần.
Streaming mang thêm nghĩa vụ lifecycle: giới hạn message size và buffer, truyền cancellation, áp deadline, xác thực lại client reconnect và định nghĩa khả năng resume. Backpressure trong một HTTP/2 connection không bảo vệ database phía sau khỏi quá nhiều stream đồng thời.
Thiết kế evolution cùng observability
Quy tắc compatibility nên được viết trước breaking change đầu tiên. Đội REST thường version representation bằng path, media type hoặc compatibility policy tường minh. Thêm field vào response nhìn chung an toàn với tolerant reader, nhưng đổi ý nghĩa, xóa field, thu hẹp input được chấp nhận hoặc đổi identifier đều là breaking change. OpenAPI diff check và consumer contract test có thể thực thi policy này.
GraphQL thường tiến hóa một schema duy nhất: thêm field và enum value cẩn thận, deprecate field cũ kèm lý do, đo mức sử dụng, migrate client đã biết, sau đó chỉ xóa theo thời hạn đã thống nhất. Thêm enum value vẫn có thể làm hỏng client sinh exhaustive switch. Schema registry và operation check cho biết thay đổi dự kiến có ảnh hưởng persisted query đang chạy ở production hay không.
Protobuf evolution phụ thuộc vào field number. Không bao giờ tái sử dụng number hoặc name đã xóa; hãy reserve chúng. Field mới dùng number mới, wire type phải được giữ nguyên, và không đổi unary method thành streaming method. Package version như commerce.v1 làm breaking revision có chủ đích trở nên dễ thấy. Generated client cải thiện ergonomics và type safety, nhưng việc phát hành code cho nhiều ngôn ngữ, pin runtime version và xử lý version skew sẽ trở thành cam kết của platform.
Observability phải khôi phục operation identity thường bị infrastructure che khuất. Hãy ghi route template có cardinality thấp, GraphQL operation name hoặc persisted hash, và fully qualified gRPC method. Truyền trace context cùng deadline qua gateway. Đo request count, latency, error rate, saturation, payload size và cancellation. Với GraphQL, thêm resolver timing, fan-out, query complexity và số data-source call. Với stream, theo dõi active stream, message rate, duration, reconnect và termination status.
Không đưa raw GraphQL query, URL tùy ý, customer ID hoặc protobuf payload vào metric label. Chúng tạo cardinality không giới hạn và có thể làm rò dữ liệu nhạy cảm. Giá trị chi tiết chỉ nên nằm trong log hoặc trace được sampling và kiểm soát truy cập.
Dùng constraint để lập decision matrix
Scorecard làm các giả định có thể review. Gán trọng số 1 đến 5 cho từng tiêu chí tại boundary, chấm mỗi lựa chọn từ 1 đến 5, nhân hai giá trị rồi ghi lại bằng chứng. Con số không tự tạo ra sự thật; nó làm lộ bất đồng để prototype hoặc load test giải quyết.
| Tiêu chí | Ưu tiên REST khi… | Ưu tiên GraphQL khi… | Ưu tiên gRPC khi… |
|---|---|---|---|
| Tập consumer | Đối tác và client chưa biết cần HTTP phổ biến, bền vững | Product client đã biết và view shape đổi thường xuyên | Cả hai đầu đều là service được kiểm soát |
| Hình dạng domain | Resource operation và HTTP semantics rõ ràng | Consumer cần duyệt, ghép một graph liên kết | Command, typed method và message flow rõ hơn |
| Caching | CDN và intermediary cache là trung tâm | Client entity cache hoặc resolver cache là đủ | Chấp nhận service-local cache tường minh |
| Streaming | SSE/WebSocket hoặc download đáp ứng nhu cầu | Product subscription phù hợp khả năng gateway | Stream tốc độ cao hoặc hai chiều là cốt lõi |
| Latency/throughput | JSON overhead không đáng kể | Giảm round trip bù được resolver cost | Compact message và lưu lượng RPC lớn quan trọng |
| Governance | OpenAPI review và endpoint ownership đã vận hành tốt | Có nguồn lực cho schema registry và graph ownership | Có nguồn lực review protobuf và phân phối generated client |
| Debugging | Traffic dễ đọc và công cụ cơ bản là quan trọng | Graph explorer và operation tooling hợp với đội | Binary-aware tooling và distributed tracing đã trưởng thành |
| Browser/edge | Direct compatibility là bắt buộc | HTTPS trực tiếp cùng BFF/gateway là tự nhiên | Chấp nhận translation proxy cùng giới hạn của nó |
Năng lực đội ngũ là production constraint, không phải điều phải né tránh. GraphQL cần người biết chặn N+1 query, thiết kế field authorization, giới hạn query cost và vận hành schema change. gRPC cần protobuf governance, code-generation pipeline, hiểu proxy và tracing qua binary call. REST cần resource modeling có kỷ luật, HTTP behavior chính xác và khả năng giữ endpoint nhất quán. Hãy chọn độ phức tạp mà đội sở hữu có thể duy trì lúc 03:00, không chỉ demo được trong workshop.
Khi hai lựa chọn vẫn gần nhau, hãy làm thin vertical prototype. Nó cần authentication, một read tiêu biểu, một write, một failure, tracing, deployment qua gateway thật và load test có hình dạng production. Những điểm tích hợp khó phân biệt phương án tốt hơn một checklist tính năng rộng.
Kết hợp protocol tại boundary có chủ đích
Một protocol không cần lan qua mọi layer. Kiến trúc phổ biến có thể cung cấp REST cho đối tác, GraphQL cho ứng dụng first-party và gRPC giữa internal service. Edge đóng vai trò anti-corruption layer: nó chuyển đổi identity, deadline, error và domain representation mà không làm lộ mọi method nội bộ.
type OrderView = {
id: string;
status: "PENDING" | "PAID" | "SHIPPED";
total: { amount: string; currency: string };
};
type GrpcOrder = {
id: string;
status: OrderView["status"];
totalMinorUnits: bigint;
currency: string;
};
interface OrdersGrpcClient {
getOrder(
request: { orderId: string },
options: { signal: AbortSignal; deadlineMs: number },
): Promise<GrpcOrder>;
}
declare function isGrpcNotFound(error: unknown): boolean;
function formatMinorUnits(value: bigint): string {
const sign = value < 0n ? "-" : "";
const absolute = value < 0n ? -value : value;
return `${sign}${absolute / 100n}.${(absolute % 100n)
.toString()
.padStart(2, "0")}`;
}
async function orderResolver(
_parent: unknown,
args: { id: string },
context: { orders: OrdersGrpcClient; signal: AbortSignal },
): Promise<OrderView | null> {
try {
const order = await context.orders.getOrder(
{ orderId: args.id },
{ signal: context.signal, deadlineMs: 800 },
);
return {
id: order.id,
status: order.status,
total: {
amount: formatMinorUnits(order.totalMinorUnits),
currency: order.currency,
},
};
} catch (error) {
if (isGrpcNotFound(error)) return null;
throw error;
}
}
Adapter chủ đích map gRPC NOT_FOUND sang GraphQL nullability, truyền cancellation cùng deadline, giữ chính xác giá trị int64 bằng bigint và trả type hướng sản phẩm thay vì để lộ generated message.
Hãy tránh protocol chain vô tình. Một GraphQL request fan-out qua REST gateway rồi đến nhiều gRPC service có thể nhân retry, deadline và khoảng trống telemetry. Mỗi boundary cần một owner, compatibility policy được công bố, fan-out có giới hạn và translation đủ nông.
Điều cần nhớ
- Chọn theo từng boundary và consumer, không chọn một lần cho toàn tổ chức.
- Dùng REST khi HTTP semantics phổ biến, public integration, caching và sự đơn giản vận hành chiếm ưu thế.
- Dùng GraphQL khi product client đã biết cần composition linh hoạt và đội ngũ đủ sức quản trị query cost cùng schema evolution.
- Dùng gRPC khi các service được kiểm soát cần generated contract, call lưu lượng cao gọn nhẹ hoặc first-class streaming.
- Đánh giá error, retry, deadline, caching, observability và compatibility trước khi so sánh cú pháp happy path.
- Xem browser gateway, proxy, phân phối generated client và chuyên môn đội ngũ là chi phí kiến trúc thực tế.
- Ưu tiên mixed architecture có chủ đích khi constraint bên ngoài và nội bộ khác nhau, với adapter tường minh để bảo toàn ý nghĩa domain.