On this page
REST and API Design
REST is not a specification, but a set of architectural constraints (HATEOAS, statelessness, uniform interface). GraphQL and gRPC each make trade-offs against REST in different constraint dimensions—choosing between them depends on "who decides the response shape" and "performance sensitivity."
Overview
REST was proposed by Roy Fielding in his 2000 doctoral dissertation—it is not a protocol but an architectural style that advocates modeling system state as "resources," using existing HTTP method semantics (GET/POST/PUT/DELETE) and status codes to manipulate them, rather than inventing a custom RPC. Twenty years later, REST remains the default paradigm for Web APIs (GitHub/Stripe/Twilio), while GraphQL (Facebook 2015) and gRPC (Google 2016) fill the gaps in scenarios where REST is weak: "variable client field requirements" and "high-performance internal RPC." The difficulty in building a good API has never been about choosing a style, but rather consistency: predictable URLs, correct method/status code semantics, unified error handling and pagination, and clear versioning and compatibility strategies.
Six REST Constraints
1. Client-Server : Separation of UI and data, allowing independent evolution
2. Stateless : Each request carries all session context → server stores no sessions → easy horizontal scaling
3. Cacheable : Responses explicitly declare cacheability (Cache-Control / ETag)
4. Uniform Interface : Unified resource identification and operation semantics (see method table below)
5. Layered System : Client is unaware of backend layers (CDN / LB / API gateway)
6. Code on Demand : (Optional) Server can distribute executable code to extend the client
Stateless is the constraint with the most real-world consequences: it allows any stateless instance to handle any request, which is the root cause of REST's ability to easily integrate with CDNs, load balancers, and auto-scaling—the trade-off is that authentication state must be included in every request (Bearer token), which is also the soil in which JWT thrives.
Method Semantics, Status Codes, and Idempotency
Idempotency (the result remains unchanged upon repeated execution) directly determines whether the client can safely retry—whether it can blindly resend after a network timeout depends entirely on whether the method is idempotent.
Method Semantics Safe Idempotent Typical Success Code
GET Read Yes Yes 200
POST Create/Action No No 201 (+ Location) / 200
PUT Full Replace No Yes 200 / 204
PATCH Partial Update No No* 200
DELETE Delete No Yes 204 (returning 404 on subsequent deletes is also an idempotent result)
(* Whether PATCH is idempotent depends on body semantics; JSON Merge Patch is usually idempotent, while array operations in JSON Patch are not)
Don't just use 200/500 for status codes:
201 Created / 202 Accepted (async) / 204 No Content
400 Bad Request · 401 Unauthorized · 403 Forbidden · 404 Not Found
409 Conflict · 422 Unprocessable Entity · 429 Too Many Requests (with Retry-After)
500 Internal Server Error · 503 Service Unavailable
Concurrency Control: Use
ETag+If-Matchfor optimistic locking—the client sends the version it read, and if the server's version has changed, it returns412 Precondition Failed, preventing "last write wins" overwrites.
Resource Modeling and URL Design
Collection GET /users List (filter/sort/paginate)
Create POST /users 201 + Location: /users/42
Single GET /users/42
Replace PUT /users/42
Patch PATCH /users/42
Delete DELETE /users/42 204
Sub-resource GET /users/42/orders
Action POST /users/42/actions/reset-password Non-CRUD action (explicitly labeled)
Anti-patterns:
GET /getUser?id=42 Verbs in URL (HTTP methods are already verbs)
POST /createUser Redundant (POST implies creation)
GET /users?action=delete Dangerous (GET must be safe; deletion should use DELETE)
Error Handling and Pagination (Underrated Design)
Error formats should be machine-readable and consistent across the site. The industry is converging on RFC 9457 (Problem Details):
Two mainstream pagination approaches, choose based on scenario:
Offset/limit : ?offset=40&limit=20 Simple to implement, supports jumping pages; slow for deep pagination + data drift when pages change
Cursor(keyset): ?after=<opaque>&limit=20 Stable and efficient, suitable for infinite scroll/large datasets; cannot jump pages
HATEOAS
The "perfect score" constraint of REST: responses embed links to available actions, driving the client flow by following links rather than hardcoding URL rules.
Ideally, the server can freely change URLs and use state to control available actions. In reality, most APIs do not implement full HATEOAS—clients usually still hardcode paths based on documentation, and the benefit does not justify the complexity. However, its degraded forms are useful: next links for pagination, and embedding sub-resource URLs within resources are practical remnants of HATEOAS thinking.
OpenAPI / Swagger
OpenAPI (formerly Swagger) is a machine-readable specification for describing REST APIs (YAML/JSON), serving as the de facto standard for REST engineering:
paths:
/users/{id}:
get:
parameters:
responses:
'200':
The value lies in deriving the entire toolchain from a single spec: Swagger UI interactive documentation, client/server codegen, request validation and mocking, and contract testing. Spec-first (write the contract before implementation) is recommended, enabling parallel frontend/backend development and treating the contract as a test.
REST vs GraphQL vs gRPC
It's not about one replacing the other, but rather their respective best-fit scenarios:
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Transport/Encoding | HTTP/1.1+ · JSON | HTTP · JSON | HTTP/2 · Protobuf (binary) |
| Data Fetching Model | One endpoint per resource | Single endpoint, client declares fields | Strongly typed RPC methods |
| Pain Points Solved | General purpose, cacheable | Over/under-fetching, variable fields | High performance, streaming, strict contracts |
| Caching | Native HTTP (URL+ETag) | Difficult (single POST endpoint) | Self-managed |
| Browser Direct Access | Native | Native | Requires gRPC-web via proxy translation |
| Typical Scenarios | Public APIs, CRUD | Rich frontends/mobile aggregating multiple sources | Internal microservices, low latency, bidirectional streaming |
gRPC-web exists because browsers cannot directly send native gRPC (lacking access to underlying HTTP/2 frame control), requiring a proxy like Envoy to translate between gRPC and browser-compatible encodings. Therefore, gRPC is mostly used for internal service-to-service communication over private networks, while external interfaces often wrap a REST/GraphQL gateway.
Versioning Strategy
URL path /api/v1/users Most common: intuitive, cache-friendly, easy routing ← Recommended
Header Accept: application/vnd.api+json; version=1 More "pure", but debugging/caching is cumbersome
Query /api/users?version=1
More important than "where to put it" is compatibility discipline: adding fields is a compatible change and does not require a version bump; deleting/changing field semantics, changing required fields, or changing types are breaking changes that require a major version bump and a clear deprecation period for the old version (Deprecation / Sunset headers).
References
- REST: Roy Fielding's dissertation (2000), Chapter 5
- OpenAPI: openapis.org · Swagger Editor · Error format RFC 9457
- GraphQL: graphql.org/learn · Apollo Documentation
- gRPC: grpc.io · grpc-web (github.com/grpc/grpc-web)
Keywords: REST, statelessness, idempotency, ETag, RFC 9457 Problem Details, cursor pagination, HATEOAS, OpenAPI, Swagger, GraphQL, over-fetching, gRPC, gRPC-web, Protobuf, API versioning