#TypeScript#DevOps

API Design: How to Choose REST, GraphQL, or gRPC

Choosing the wrong API style makes late-stage changes impossible. This article clarifies the philosophies and fit of REST (resources/universal/simple), GraphQL (flexible fetching/frontend-driven), and gRPC (high performance/internal microservices), plus scenario-based selection and REST's own pitfalls (versioning/status codes/nesting).

Key Points

  • Topic Focus: TypeScript、DevOps
  • Suitable for: indie hackers, full-stack engineers, AI application developers
  • Core value: quickly understand background, practical paths, common pitfalls, and engineering advice
Author @admin·Published July 26, 2026·Updated July 26, 2026

Online discussions about API style almost always devolve into "REST vs GraphQL vs gRPC, which is strongest" tribalism. This kind of discussion is meaningless. These three styles are not three answers to the same question; they are three philosophies designed for different scenarios—choosing the wrong one doesn't cost "a bit of performance", it means late-stage immutability: you can't add fields, clients break, and internal service calls get an extra layer.

This article isn't about who wins; it's about how to choose by scenario, and the pitfalls each style brings.

1. The philosophical differences: resources, queries, methods

To understand the differences between the three, first look at how each defines "what an API is".

  • REST: Resource-oriented. The entire API is a set of "nouns" (resources), and you perform actions on these nouns with HTTP methods (GET/POST/PUT/DELETE). The essence is "transferring the state of a set of resources" (Representational State Transfer).
  • GraphQL: Query-driven. The client writes a query describing "what I want", and the server returns it based on that query. The essence is "querying a Graph".
  • gRPC: Method invocation. The essence is Remote Procedure Call (RPC); you call remote methods like calling local functions, with strongly typed messages for both input and output.

A comparison table first; details below:

DimensionRESTGraphQLgRPC
Abstraction modelResource (noun)Query (Graph)Method invocation
ProtocolHTTP/1.1 or HTTP/2HTTP (usually POST)HTTP/2
Data formatJSON (mainstream)JSONProtobuf (binary)
Data-fetching granularityFixed endpointsClient on-demandFixed message fields
Browser friendlyExcellentGoodPoor (needs gRPC-Web or gateway)
CachingHTTP cache natively supportedHard (POST has no cache semantics)None
Typical scenariosPublic APIs, CRUDFrontend aggregation, multi-variant UIInternal microservices, low latency

Remember this table and you basically remember where each of the three sits.

2. REST: mature, universal, but with the most pitfalls

REST's advantage is obvious: HTTP-native. It directly reuses HTTP's semantics—methods, status codes, headers, caching. This means CDNs, reverse proxies, browser caches, curl, and Postman all come for free. Public-facing APIs are almost REST by default, because the ecosystem is the thickest and the learning cost is the lowest.

The suitable scenarios are clear: public APIs, third-party integrations, CRUD-heavy business. An order system—create order, query order, change status, delete—written in REST looks as clean as a textbook.

But REST's pitfalls are real and many, and they're all "don't notice during design, discover during runtime" pitfalls:

  1. How to manage versions. Once online, changing a field is a breaking change. Common practice is to put the version in the URL (/api/v1/orders), or use a header (Accept: application/vnd.myapi.v1+json). Both work; the key is to think it through from the start, don't wait until v3 to remember.
  2. Status code semantics get abused. The most typical is HTTP 200 wrapping a { code: 500, msg: "failed" }. This completely hollows out the HTTP status code. The correct approach is to make HTTP status codes meaningful: 201 Created, 400 Bad Request, 404 Not Found, 422 Unprocessable Entity, 429 Too Many Requests; business error details go in the body.
  3. Over-fetching / under-fetching. A list endpoint returns 30 fields, the frontend uses only 3—this is over-fetching; to see order details you have to call 5 more endpoints—this is under-fetching. This is the pain point REST gets criticized for most by GraphQL.
  4. How to design nested resources. /users/123/orders or /orders?userId=123? There's no standard answer, but nesting within two levels is a commonly acceptable upper limit; beyond that, use query parameters or independent endpoints.

3. GraphQL: flexibility is its strength, and also its burden

The core problem GraphQL solves is the "over/under-fetching" from the previous section. The frontend writes its own query, fetching exactly the fields it wants, and one request can bring back multiple resources together. Plus a strongly typed Schema lets the frontend get precise type definitions for autocompletion.

graphql
query {
  user(id: "123") {
    name
    orders(last: 3) {
      total
      items { name price }
    }
  }
}

One request fetches three resources—user, orders, order items—with fields as minimal as possible. This experience is a step-function improvement for frontend-variable, multi-source-aggregation scenarios (typical: e-commerce homepage, content platform, Dashboard).

But GraphQL's costs are equally real:

  1. N+1 problem. Querying a list of users, each user related to orders, a naive implementation will fire N order queries. You must use DataLoader for batch loading; this is additional cognitive burden.
  2. Complex query attacks. A malicious client can construct deeply nested queries to crash the server. You must do query depth limits and complexity scoring.
  3. Hard to cache. GraphQL uses POST, and POST has no HTTP cache semantics. You rely on client caching (Apollo Client, Relay) and server-side persisted queries to compensate.
  4. High server-side implementation cost. Schema design, resolver orchestration, permission granularity (every field needs authorization consideration)—much more complex than writing a REST controller.

Don't adopt GraphQL just to "save the frontend a few calls". If the number of endpoints is stable and frontend scenarios are single, REST is entirely sufficient; GraphQL is a burden instead.

4. gRPC: the performance king for internal services

gRPC takes a different path. It uses Protobuf for binary serialization (30%-70% smaller than JSON, parsing an order of magnitude faster), runs on HTTP/2 (multiplexing, header compression), and natively supports bidirectional streaming. The interface contract is defined through .proto files, generating multi-language clients.

protobuf
service OrderService {
  rpc CreateOrder(CreateOrderRequest) returns (Order);
  rpc SubscribeOrders(SubscribeRequest) returns (stream OrderEvent);
}

This combination makes gRPC almost unbeatable in internal microservice communication, low-latency scenarios: service meshes, real-time data streams, high-frequency trading, internal RPC gateways. Microservices calling each other, latency compressed from tens of milliseconds to single-digit milliseconds—gRPC is key.

gRPC's streaming capability deserves special mention. Four modes: unary call, server streaming, client streaming, bidirectional streaming. Subscribing to real-time quotes, log reporting, chat room message push—these with REST either require polling or WebSocket, while gRPC's bidirectional streaming is natively supported and type-safe.

But gRPC isn't suitable for direct external use. Browsers are unfriendly to HTTP/2 trailers and Protobuf handling; usually you need gRPC-Web or a gRPC-Gateway to expose it as REST/JSON. Debugging is also harder than REST—you can't directly curl a binary body; usually you rely on reflection plus a command-line tool like grpcurl to get through.

5. Selection decision: don't take sides, look at the scenario

Converge the judgments above into one sentence: look at who this API faces.

  • Facing external, third-party, browser → REST. Thick ecosystem, cacheable, easy to debug, the default choice for public APIs.
  • Facing your own frontend, variable UI, needing multi-source aggregation → GraphQL. Frontend fetches on-demand, backend gets fewer ad-hoc requests.
  • Facing internal inter-service communication → gRPC. Performance, streaming, strongly typed contracts.

In practice these three are often combined. A fairly common architecture is:

text
Browser ──REST/GraphQL──> BFF (Backend for Frontend) ──gRPC──> Internal microservices

Use REST or GraphQL externally for friendliness; use gRPC internally for performance. This isn't fence-sitting; it's layering: each layer uses the most appropriate tool.

6. REST best practices: get the details right

If you chose REST (the choice for most external APIs), get these details right and your API quality surpasses 80% of peers:

  1. Resources use plural nouns: /orders instead of /getOrder, /createOrder. Leave actions to HTTP methods.
  2. Version in the URL: /api/v1/orders. Simple, explicit, easy to debug, more universal than header versioning.
  3. Semantic status codes: 201 creation success, 204 no content, 400 request format error, 422 business validation failure, 401 unauthenticated, 403 no permission, 429 rate limited, 500 server error. Don't return 200 for everything.
  4. Pagination uses cursors, not offset: offset misaligns when data changes (missing or duplicate data), and on large tables it's catastrophically slow (OFFSET 100000 scans 100k rows). Cursor pagination is stable and fast, and naturally supports "incremental fetching". The return structure carries nextCursor, which the frontend includes next time.
  5. Filtering and sorting go through query parameters: /orders?status=paid&sort=-createdAt. Hand expressiveness to query parameters, rather than designing RPC-style endpoints like /getPaidOrdersSortedByDate.
  6. Unified error format: regardless of which endpoint errors, the return structure is consistent.
json
{
  "error": {
    "code": "ORDER_NOT_FOUND",
    "message": "Order 123 does not exist",
    "details": { "orderId": "123" }
  }
}

7. API documentation isn't optional

Last point, often overlooked: an API without documentation equals not designed.

  • REST uses OpenAPI (Swagger)—a .yaml describes each endpoint, parameter, response, generating interactive docs and client SDKs.
  • GraphQL's Schema is itself the documentation, paired with GraphiQL / Apollo Studio for automatic visualization.
  • gRPC's .proto file is the contract; generating multi-language stubs also generates documentation.

Documentation isn't written for the doc tool; it's written for the caller and yourself three months later. A runnable OpenAPI lets the frontend advance independently of the backend, lets SDKs auto-generate, and lets contract testing land.

Summary

  • REST suits external use: HTTP-native, cache-friendly, thickest ecosystem; pitfalls in versioning, status code semantics, over/under-fetching.
  • GraphQL suits frontend-variable scenarios: on-demand data fetching, strongly typed Schema, one-time multi-source aggregation; cost is N+1, complex query protection, hard caching.
  • gRPC suits internal services: Protobuf + HTTP/2 + streaming, low latency, strong contracts; not suitable for direct external use, needs a Gateway.
  • The three often combine: external REST/GraphQL, internal gRPC, BFF in between to bridge.
  • Selection criterion is only one: look at who this API faces, not which style is more "advanced".

Key quote: Good API design isn't about which style you chose; it's about each layer using the most appropriate tool, and treating contracts (docs/Schema/proto) as first-class citizens—styles can be debated, contracts cannot be skipped.

Related

Related Content