Design high-quality REST, GraphQL, and gRPC APIs with contract-first workflows, resource modelling, versioning, pagination, error contracts, and OpenAPI authoring. Use when designing a new API, reviewing an existing API contract, defining error schemas, choosing between REST / GraphQL / gRPC, or writing an OpenAPI specification. Triggers: "API design", "REST API", "OpenAPI", "swagger", "versioning", "pagination", "error contract", "gRPC", "GraphQL", "contract-first", "resource model", "API review", "HTTP API".
| Criteria | REST | GraphQL | gRPC |
|---|---|---|---|
| Consumers | Public / multiple clients | One backend, many varied frontends | Internal service-to-service |
| Query flexibility | Fixed per endpoint | Client-defined (over/under-fetch solved) | Fixed per RPC method |
| Tooling maturity | Excellent | Good | Good (requires codegen) |
| Streaming | SSE / WebSocket add-on | Subscriptions | Native (server/client/bidi) |
| Caching | HTTP cache out of the box | Complex (persisted queries) | No HTTP cache |
| Browser native | Yes | Yes | No (needs gRPC-Web proxy) |
| Schema / contract | OpenAPI | GraphQL SDL | Protocol Buffers |
| Best for | Standard CRUD, public APIs | Data-rich SPAs, BFF pattern | Microservice RPC, high-throughput |
/<version>/<resource-collection>/<id>/<sub-resource>
/v1/orders # collection
/v1/orders/{orderId} # single resource
/v1/orders/{orderId}/items # sub-resource collection
/v1/orders/{orderId}/items/{itemId}
Rules:
/orders, not /order)/order-lines, not /orderLines)/v1/) — visible, cacheable, easy to route/v1/cancelOrder → POST /v1/orders/{id}/cancellation)| Method | Semantics | Idempotent | Safe | Body |
|---|---|---|---|---|
GET | Retrieve resource(s) | Yes | Yes | No |
POST | Create new resource / trigger action | No | No | Yes |
PUT | Replace resource entirely | Yes | No | Yes |
PATCH | Partial update | No* | No | Yes |
DELETE | Remove resource | Yes | No | No |
*PATCH is idempotent if the operation is absolute, not relative.
Action resources — when an operation doesn't map cleanly to a resource:
POST /v1/orders/{id}/cancellation # instead of PATCH with status=cancelled
POST /v1/documents/{id}/publication # publish action
POST /v1/accounts/{id}/password-reset # trigger email
| Scenario | Code |
|---|---|
| Created successfully | 201 Created + Location header |
| Retrieved / updated | 200 OK |
| No content (DELETE, some PUT) | 204 No Content |
| Async operation accepted | 202 Accepted + polling URL in body |
| Validation failure | 400 Bad Request |
| Not authenticated | 401 Unauthorized |
| Authenticated but not authorised | 403 Forbidden |
| Not found | 404 Not Found |
| Method not allowed | 405 Method Not Allowed |
| Conflict (duplicate, stale ETag) | 409 Conflict |
| Precondition failed (ETag mismatch) | 412 Precondition Failed |
| Unprocessable entity (semantic error) | 422 Unprocessable Entity |
| Rate limited | 429 Too Many Requests + Retry-After header |
| Internal error | 500 Internal Server Error |
| Service unavailable | 503 Service Unavailable + Retry-After |
All error responses must use a consistent machine-readable structure. Recommended (RFC 9457 / Problem Details):
{
"type": "https://api.example.com/errors/validation-failed",
"title": "One or more validation errors occurred",
"status": 400,
"detail": "The 'quantity' field must be greater than zero",
"instance": "/v1/orders/ord_123",
"traceId": "00-abc123-def456-00",
"errors": {
"quantity": ["Must be greater than zero"],
"sku": ["Required field"]
}
}
Rules:
type — URI uniquely identifying the error class (links to docs if possible)title — human-readable, stable label for the error classdetail — specific to this occurrence, safe to show in logstraceId — always include for server errors; aids supportGET /v1/orders?limit=25&cursor=eyJpZCI6MTIzfQ==
{
"data": [ ... ],
"pagination": {
"nextCursor": "eyJpZCI6MTQ4fQ==",
"hasMore": true
}
}
GET /v1/reports?page=3&pageSize=20
{
"data": [ ... ],
"pagination": {
"page": 3,
"pageSize": 20,
"totalCount": 347,
"totalPages": 18
}
}
Cursor wins when: data is large, real-time, or frequently updated. Offset produces inconsistent results on moving datasets.
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path (recommended) | /v2/orders | Explicit, cacheable, routable | URL changes |
| Header | API-Version: 2024-01-01 | Clean URLs | Less visible, harder to test |
| Content negotiation | Accept: application/vnd.api.v2+json | Purist REST | Complex client code |
Versioning rules:
Deprecation header + Sunset header (RFC 8594)api/
├── openapi.yaml # root spec
├── components/
│ ├── schemas/ # reusable models
│ ├── responses/ # reusable response objects
│ ├── parameters/ # reusable query/path params
│ └── securitySchemes/ # auth definitions
└── paths/
├── orders.yaml
└── products.yaml
operationId — unique, verb-noun format (create-order, list-orders)summary — one sentencetags — group by resourcedescription, example, constraints200/201 response documented with complete schema$ref to avoid repetition — define once in components/schemas/required: [...]nullable: true (OAS 3.0) or type: [string, null] (OAS 3.1) — never omit nullabilityexample or examples on every propertyreadOnly: true on server-generated fields (id, createdAt)writeOnly: true on credential fields (password)* on authenticated APIs)traceId in all error responsesGET /health) returns structured JSONspectral lint openapi.yaml)// Naming: Services = PascalCase, RPCs = PascalCase, fields = snake_case
service OrderService {
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
rpc ListOrders(ListOrdersRequest) returns (stream Order); // server streaming
}
message CreateOrderRequest {
string customer_id = 1;
repeated OrderLine line_items = 2;
}
Rules:
google.protobuf.Timestamp for time, never stringsgoogle.protobuf.FieldMask for partial updatesgoogle.rpc.Status + google.rpc.ErrorInfopackage orders.v1;When to use: BFF (backend-for-frontend) pattern; heterogeneous clients needing different data shapes.
Schema design rules:
input types for mutations, not inline argsedges, node, pageInfo)null for lists — return empty array []