---
title: REST and API Design
url: https://doc.liz6.com/en/networking/06-HTTP/05-rest-and-api-design
locale: en
area: networking
tags:
- networking
- HTTP
date: 2026-06-30
modified: 2026-07-16
description: 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."
---

# 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)**:

```json
{ "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.

```json
{ "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:

```yaml
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:

| | 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*
