14 min read #networking #HTTP
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-Match for optimistic locking—the client sends the version it read, and if the server's version has changed, it returns 412 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):

{ "type": "https://api.example.com/errors/insufficient-funds",
  "title": "Insufficient funds", "status": 409,
  "detail": "Balance 10 < required 50", "instance": "/accounts/42/transfer/abc",
  "errors": [ {"field": "amount", "message": "exceeds balance"} ] }

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.

{ "id": 42, "state": "pending",
  "_links": { "self":   {"href": "/orders/42"},
              "cancel": {"href": "/orders/42/actions/cancel"},
              "pay":    {"href": "/orders/42/payment"} } }

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: [{ name: id, in: path, required: true, schema: { type: integer } }]
      responses:
        '200': { content: { application/json: { schema: { $ref: '#/components/schemas/User' } } } }

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:

RESTGraphQLgRPC
Transport/EncodingHTTP/1.1+ · JSONHTTP · JSONHTTP/2 · Protobuf (binary)
Data Fetching ModelOne endpoint per resourceSingle endpoint, client declares fieldsStrongly typed RPC methods
Pain Points SolvedGeneral purpose, cacheableOver/under-fetching, variable fieldsHigh performance, streaming, strict contracts
CachingNative HTTP (URL+ETag)Difficult (single POST endpoint)Self-managed
Browser Direct AccessNativeNativeRequires gRPC-web via proxy translation
Typical ScenariosPublic APIs, CRUDRich frontends/mobile aggregating multiple sourcesInternal 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