API Design Overview

A synthesis of REST, GraphQL, and gRPC — decision guide for selecting the right API style, managing contracts, versioning, and maturity.


The Three Major API Styles

Modern APIs fall into three dominant styles, each with different strengths:

RESTGraphQLgRPC
ProtocolHTTP/1.1HTTP/1.1 (HTTP/2 optional)HTTP/2
PayloadJSON / XMLJSONProtocol Buffers (binary)
ContractOptional (OpenAPI/Swagger)GraphQL Schema (built-in).proto file (built-in)
Query flexibilityFixed shapes per endpointClient-defined queriesFixed method signatures
StreamingLimited (SSE, WebSocket)Subscriptions (WebSocket)Native (4 modes)
HTTP cachingNativeNon-trivial (POST)Not applicable
Browser supportFullFullNeeds gRPC-Web proxy
Type safetyOptionalBuilt-inBuilt-in
Best forPublic APIs, CRUD, broad client accessFlexible data queries, BFFInternal RPC, low latency, streaming

Decision Guide: Which API Style to Choose?

Choose REST when:

  • Building a public-facing API consumed by diverse third-party clients.
  • The API is resource-oriented (CRUD over entities: users, orders, products).
  • HTTP caching is important (CDN, browser cache for GET responses).
  • Client diversity is high (browsers, mobile, CLIs, third-party integrations).
  • Team has existing REST tooling and conventions.

Choose GraphQL when:

  • Multiple clients (mobile, web, partners) have different data needs for the same domain.
  • Eliminating over-fetching and under-fetching is a priority.
  • The API aggregates data from multiple backend services (API gateway / BFF pattern).
  • Rapid front-end iteration is required and front-end teams should own their data shape.
  • Subscription (real-time) is needed alongside queries and mutations.

Choose gRPC when:

  • Internal microservice-to-microservice communication where low latency matters.
  • The system is polyglot and needs cross-language generated clients with strict contracts.
  • Streaming is a core requirement (server push, client upload, bidirectional).
  • Bandwidth efficiency matters (binary Protobuf vs. JSON).
  • The API is not directly consumed by browsers (or gRPC-Web is acceptable).

Maturity and Contracts

REST Maturity: Richardson Maturity Model

The Richardson Maturity Model describes four levels of REST adoption:

LevelKey conceptWhat it adds
0HTTP as transportBaseline — single endpoint
1ResourcesDistinct URIs per resource
2HTTP Verbs + Status CodesCorrect use of GET/POST/PUT/DELETE, proper status codes
3HATEOASHypermedia links in responses for self-discovery

Industry reality: Most production APIs operate at Level 2 and are correctly called “HTTP APIs.” Full HATEOAS (Level 3) is rare but valuable for highly evolvable public APIs.

GraphQL and gRPC Contracts

Both GraphQL and gRPC are contract-first by design:

  • GraphQL schema defines all types and operations; changes are validated against it.
  • Protobuf .proto files define all services and messages; protoc enforces compatibility.

REST depends on external tooling (OpenAPI/Swagger) for contract definition — best practice but not enforced by the protocol.


Versioning Strategies

All three styles eventually face breaking changes. The approaches differ:

REST Versioning

Three common patterns — see API Design Principles:

  • URI versioning: /api/v1/users — most common; explicit but creates URI proliferation.
  • Header versioning: Accept: application/vnd.api.v2+json — clean URIs; less visible.
  • Query param: /users?version=2 — simple; not cache-friendly.

GraphQL Versioning

GraphQL’s philosophy is “versionless evolution”:

  • Add new fields/types freely (non-breaking).
  • Deprecate fields with @deprecated directive; provide migration guidance.
  • Avoid breaking changes; if unavoidable, introduce a new type alongside the old.
  • This works because clients request only specific fields — adding new fields doesn’t break existing queries.

gRPC Versioning

Protocol Buffers enable forward and backward compatibility through field numbering:

  • Adding new fields with new numbers is non-breaking.
  • Never remove or renumber existing fields.
  • For breaking changes: create a new service version (v2) alongside the old.
  • Proto3 defaults unknown fields to zero values, preserving compatibility.

Backward Compatibility

Common to all styles:

Non-breaking changes (safe):

  • Adding new optional fields to a response.
  • Adding new endpoints/operations.
  • Adding new optional request parameters.

Breaking changes (requires versioning):

  • Removing or renaming a field.
  • Changing a field’s data type.
  • Making optional parameters required.
  • Changing URI structure (REST) or removing a method (gRPC).

Tolerant Reader pattern: Consumer code should ignore unknown fields and not fail on additions. This is essential for forward compatibility.


Key Cross-Cutting Concerns

  • Idempotency — critical for POST (REST) and mutations (GraphQL); use idempotency keys for safe retries.
  • Authentication — JWT bearer tokens (REST/GraphQL) or gRPC metadata (typically Authorization header).
  • Rate limiting429 Too Many Requests (REST); query complexity limits (GraphQL); server-side rate limiting (gRPC).
  • Pagination — offset/limit or cursor-based (REST/GraphQL); server-streaming (gRPC for large datasets).
  • Error handling — HTTP status codes (REST); errors array in response (GraphQL); gRPC status codes (google.rpc.Status).
  • Observability — correlate requests with trace IDs; standardise logging fields.

Hybrid Approaches

Many systems combine styles:

  • gRPC internally, REST externally: microservices communicate via gRPC; a gateway exposes REST/GraphQL to clients.
  • GraphQL as a BFF layer: a GraphQL API aggregates multiple downstream REST or gRPC services.
  • REST for resources, gRPC for streaming: REST for CRUD endpoints, gRPC for real-time data feeds.