Quick reference
The fastest decision points and the contract artifact that should exist before implementation starts.
Use this as the "cheat within the cheat". The design choice should be obvious after one scan.
| Situation | Default choice | Why | Contract artifact |
|---|---|---|---|
| CRUD on stable resources | REST | HTTP methods, caching, proxies, and tooling all fit naturally. | OpenAPI document plus resource model |
| Client needs nested, shaped reads | GraphQL | One request can fetch the exact fields and related objects needed. | GraphQL schema and field-level auth policy |
| Internal service-to-service RPC | gRPC | Binary transport, generated stubs, deadlines, and streaming are first-class. | .proto service and message definitions |
| Outbound event notifications | Webhooks | The producer controls retries; the consumer controls idempotency. | Event schema, signature scheme, replay policy |
| Browser push from server to client | SSE | Simple one-way stream over HTTP; good for live dashboards and status. | Stream endpoint and reconnect policy |
| Two-way low-latency interaction | WebSockets | Bidirectional control or chat when HTTP request/response is too coarse. | Message envelope, heartbeat, backpressure policy |
| Need a machine-readable HTTP contract | OpenAPI | Design-first docs, client generation, validation, and review gates. | OpenAPI 3.2.0 document |
| Need a versioned callback contract | Webhook callback object | Callbacks belong in the contract, not in the integration folklore. | OpenAPI callback object or provider webhook spec |
API style decision matrix
Compare the four primary styles across transport, caching, schema, browser fit, streaming, and failure modes.
| Style | Best fit | Strength | Weakness | Contract / tooling | When not to use |
|---|---|---|---|---|---|
| REST | Public or semi-public resource APIs | HTTP semantics, cacheability, broad tooling | Overfetching, endpoint sprawl, version pressure | OpenAPI, HTTP status codes, ETags | When the client must freely shape deeply nested reads |
| GraphQL | Client-driven aggregation and mobile/web backends | Single endpoint, exact field selection, schema introspection | Caching and authorization are harder; query planning can hurt | GraphQL schema, operation docs, complexity limits | When the API is simple CRUD or you need proxy-friendly caching |
| gRPC | Service-to-service RPC and streaming | Binary efficiency, stubs, deadlines, streaming | Browsers need a gateway; humans do not handcraft it easily | .proto, generated code, reflection/health checks |
When you need broad browser compatibility without a proxy |
| Webhooks | Asynchronous notifications to other systems | Push-based integration, decoupled timing, event fan-out | Delivery is retried, duplicated, and occasionally out of order | Event schema, signature verification, replay log | When the consumer expects request/response semantics |
Core design primitives
The nouns you should decide before debating style: resource, command, event, subscription, callback, and retry identity.
Resource
A stable thing with an identity, such as an order, invoice, or shipment.
- Example:
/orders/12345for the order record. - Gotcha: if the URL names an action, it is probably not a pure resource.
Command
A request to cause a state change, such as capture payment or cancel order.
- Example: a cancel request for order
12345. - Gotcha: commands need idempotency if callers may retry.
Event
A fact that already happened, such as invoice.paid.
- Example: event
payment_intent.succeededwith a durable event ID. - Gotcha: events are immutable history, not editable state.
Subscription
A long-lived read that receives future data or events over time.
- Example: GraphQL subscription for order status changes.
- Gotcha: subscriptions are not a replacement for durable delivery logs.
Callback
A server-initiated request to a consumer endpoint, usually a webhook.
- Example:
POST https://hooks.example.com/stripe. - Gotcha: the receiver must expect retries and duplicate deliveries.
Idempotency token
A key that makes a retried write safe to repeat.
- Example:
Idempotency-Key: 3b4f1f4c-8c2b-4f4b-b6ee-4b1fbe4e6f8c. - Gotcha: use one token per intended operation, not per HTTP attempt.
Correlation / trace ID
A shared identifier that ties a request chain together across services.
- Example:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00. - Gotcha: do not overload it as a business identifier.
Canonical representation
The authoritative payload shape for a resource or event at a point in time.
- Example: the serialized JSON for an order resource.
- Gotcha: internal DB shape is not the canonical API shape.
REST done well
Resource modeling, method semantics, conditional requests, pagination, and versioning that do not fight HTTP.
Fundamentals
Use nouns for resources and HTTP methods for intent: GET reads, POST creates, PUT replaces, PATCH partially updates, DELETE removes.
- Example:
GET /orders/12345orPOST /orders. - Gotcha: if every endpoint is
/doThing, you are doing RPC over HTTP.
Working knowledge
Design the response contract before coding: status codes, pagination, filtering, sorting, and headers.
- Example:
GET /orders?customer_id=123&limit=50&cursor=eyJpZCI6IjQyIn0. - Gotcha: without
ETagandIf-Match, update races become silent overwrites.
Edge & advanced
Use 202 for async jobs, 204 for empty success, and conditional requests for concurrency control.
- Example:
PUT /orders/12345withIf-Match: "v7". - Gotcha: bulk and command-like endpoints need explicit failure semantics or they become unrecoverable.
| Method / code | Definition | Example | Pitfall |
|---|---|---|---|
| GET | Safe read of a resource representation. | GET /customers/91 |
Do not make it mutate server state. |
| HEAD | GET metadata without the body. | HEAD /assets/invoice.pdf |
Useful for existence and cache checks; often overlooked. |
| POST | Create a subordinate resource or trigger a non-idempotent action. | POST /orders |
Use idempotency keys when clients may retry. |
| PUT | Replace the full representation at a known URI. | PUT /orders/12345 |
Do not send partial state unless the API explicitly defines merge semantics. |
| PATCH | Apply a partial modification. | PATCH /orders/12345 |
Define the patch media type and conflict behavior. |
| DELETE | Remove or tombstone a resource. | DELETE /sessions/abc123 |
Deletion may be asynchronous and return 202, not always 204. |
| 201 Created | A write created a resource, usually with a Location header. | POST /orders returns 201 and Location: /orders/12345 |
Returning 200 for creation hides the newly created URI. |
| 202 Accepted | The server accepted work but is not done yet. | Batch invoice generation accepted for background processing. | Never imply completion when the job can still fail. |
| 204 No Content | Success with no body. | DELETE /sessions/abc123 returns 204. |
Do not include a body and call it 204. |
| 409 Conflict | The request conflicts with current state. | Updating an order that another writer already closed. | Useful for concurrency or business-rule conflicts. |
| 412 Precondition Failed | A conditional request did not match current state. | If-Match etag mismatch on update. |
Use this for lost-update prevention, not generic validation. |
| 415 Unsupported Media Type | The payload format is not acceptable. | Client sends XML to a JSON-only endpoint. | Separate format errors from semantic errors. |
| 422 Unprocessable Content | The syntax is valid but the content fails domain validation. | Postal code fails country-specific rules. | Use only when the schema is valid and the business rule is not. |
| 429 Too Many Requests | Rate limit or abuse control. | Retry after Retry-After: 60. |
If you return 429, tell the client when to retry. |
PUT /orders/12345 HTTP/1.1
If-Match: "v7"
Content-Type: application/json
{
"status": "confirmed",
"shipping_address": {
"city": "Austin",
"region": "TX",
"postal_code": "78701"
}
}
OpenAPI should describe the endpoints, schemas, response codes, headers, and security schemes. If the contract cannot be generated or validated, the spec is too vague.
GraphQL done well
Schema-first reads, explicit mutations, cursor pagination, and field-level security that does not collapse under real traffic.
Fundamentals
GraphQL is a typed schema where clients ask for exactly the fields they need from the object graph.
- Example:
query { order(id: "12345") { id total customer { name } } } - Gotcha: GraphQL is not "REST but with JSON"; it has its own execution and error model.
Working knowledge
Serve GraphQL over HTTP with JSON bodies, use POST by default, and allow GET for query operations when appropriate.
- Example: response contains
dataand maybeerrors. - Gotcha: auth and rate limits must happen before expensive execution where possible.
Edge & advanced
Use cursor pagination, complexity limits, persisted queries, and per-field authorization to keep it stable.
- Example:
orders(first: 20, after: "YXJyYXljb25uZWN0aW9uOjE5"). - Gotcha: ignoring N+1 makes GraphQL look fast in demos and slow in production.
| Topic | Rule | Example | Gotcha |
|---|---|---|---|
| Operation types | query reads, mutation writes, subscription streams live updates. |
mutation PlaceOrder($input: PlaceOrderInput!) |
Do not use mutations as a generic RPC escape hatch without a typed input contract. |
| HTTP transport | Use POST for most operations; GET is allowed for query operations. | Content-Type: application/json |
Do not assume every intermediary will cache GraphQL the same way it caches REST. |
| Response shape | The response can include data, errors, and extensions. |
Partial data plus field errors in one response. | Partial success is normal; code must inspect both data and errors. |
| Pagination | Prefer cursor-based connections for lists that can grow. | first: 20, after: "cursor" |
Offset pagination becomes unstable on changing datasets. |
| Authorization | Enforce access by field/object boundary, not only at the root query. | A user can read their own invoice but not another tenant's email field. | Field-level leaks are easy when resolvers reuse generic loaders. |
| Complexity control | Limit depth, breadth, and cost of one query. | Cap list sizes and nested object expansions. | Unlimited introspection plus no limits becomes a denial-of-service path. |
| Schema evolution | Add fields; deprecate before removal; keep additive compatibility. | Mark oldField deprecated and add newField. |
Removing a field without a deprecation window breaks clients silently. |
query OrderSummary($id: ID!) {
order(id: $id) {
id
status
total
customer {
id
name
}
}
}
gRPC done well
Strong contracts, generated clients, streaming, and deadline-aware service-to-service calls.
Fundamentals
gRPC starts with a .proto file and generates client and server bindings from a typed service definition.
- Example:
service Orders { rpc GetOrder(GetOrderRequest) returns (Order); } - Gotcha: gRPC is not hand-written JSON over HTTP; the wire format and semantics are different.
Working knowledge
Use unary, server streaming, client streaming, or bidirectional streaming depending on flow direction.
- Example: server-streaming telemetry updates for a live job.
- Gotcha: the browser usually needs gRPC-Web or a gateway proxy.
Edge & advanced
Define deadlines, retries, and status handling so the caller can fail fast instead of hanging.
- Example: a 3 second deadline for a read path that normally finishes in 100 ms.
- Gotcha: retries without deadlines can amplify load during partial outages.
| RPC shape | Definition | Example | Use case |
|---|---|---|---|
| Unary | One request, one response. | GetOrder |
Simple reads and commands. |
| Server streaming | One request, many responses. | WatchInventory |
Live progress, telemetry, subscriptions. |
| Client streaming | Many requests, one response. | UploadTelemetry |
Batching a flow of client events. |
| Bidirectional streaming | Many requests, many responses. | Interactive control loop or sync feed. | Live collaboration, chat, or agent control. |
| Deadlines | Client-stated limit for how long the RPC may run. | deadline = now + 3s |
Prevent zombie calls and make retries bounded. |
| Status codes | gRPC returns canonical codes like OK, INVALID_ARGUMENT, UNAVAILABLE, and DEADLINE_EXCEEDED. |
Retry only if the status is retryable and the deadline allows it. | Always map business failures to a specific code, not "unknown". |
| Proto compatibility | Add fields, do not reuse field numbers, and avoid changing field types. | Add string shipping_method = 8; |
Backward compatibility depends on stable numeric tags. |
syntax = "proto3";
service Orders {
rpc GetOrder (GetOrderRequest) returns (Order);
rpc StreamOrderEvents (OrderEventRequest) returns (stream OrderEvent);
}
message GetOrderRequest {
string order_id = 1;
}
Use gRPC-Web or a gateway when the client is a browser; plain browser JavaScript cannot speak native gRPC over HTTP/2 frames directly.
Webhooks done well
Push events out reliably, verify them cryptographically, and assume duplicates, retries, and redelivery windows.
Fundamentals
A webhook is an HTTP POST to a consumer endpoint when something happens in the producer system.
- Example:
payment_intent.succeededsent tohttps://hooks.example.com/payments. - Gotcha: the producer cannot assume the consumer is online or fast.
Working knowledge
Verify signatures, respond with a 2xx quickly, then process the event asynchronously.
- Example: Stripe
Stripe-Signatureor GitHubX-Hub-Signature-256. - Gotcha: if verification happens after processing, forged traffic can waste work or trigger side effects.
Edge & advanced
Store delivery IDs, support replay, and make the consumer idempotent because retries are normal.
- Example: ignore a duplicate event by event ID after the first successful application.
- Gotcha: ordering is usually best-effort, not a guarantee.
| Delivery rule | Definition | Example | Why it matters |
|---|---|---|---|
| Fast 2xx ACK | Acknowledge as soon as the payload is safely accepted. | Return 200 or 204 before long processing. |
Prevents producer retry storms and keeps latency bounded. |
| Signature verification | Prove the event came from the expected producer and was not altered. | Verify an HMAC over the raw request body. | Protects against spoofed or tampered deliveries. |
| Idempotent consumer | A repeated delivery has no extra side effects. | Record event evt_123 before applying the state change. |
Retries and redelivery are normal; duplication must be harmless. |
| Retry policy | The producer decides how often to retry a failed delivery. | Exponential backoff over several days. | The consumer should never rely on exactly-once transport. |
| Replay / redelivery | A way to resend past events from a UI or CLI. | GitHub delivery redelivery or Stripe CLI resend. | Critical for recovery from downtime or bugs in the handler. |
| Event filtering | Subscribe only to the event types you actually handle. | Listen only to invoice.paid and invoice.payment_failed. |
Reduces noise, cost, and handler complexity. |
| Dead-letter / backlog | Keep deliveries that repeatedly fail and surface them to operators. | A failed event queue or replay dashboard. | Without a backlog, you lose failures in the dark. |
POST /webhooks/stripe HTTP/1.1
Stripe-Signature: t=1720384800,v1=...
Content-Type: application/json
{
"id": "evt_123",
"type": "payment_intent.succeeded",
"data": { "object": { "id": "pi_456" } }
}
Stripe documents automatic retries for up to three days in live mode; GitHub documents signature validation and manual redelivery. Treat those as the pattern, not the whole implementation.
Security, auth, and governance
Bearer tokens, OpenID Connect, mTLS, rate limits, and contract review keep API style from becoming API drift.
OAuth / OIDC
OAuth is for delegated access; OpenID Connect is the identity layer on top when you need login and user claims.
- Example:
Authorization: Bearer eyJ...for a protected API. - Gotcha: bearer tokens must be protected in storage and in transit.
mTLS / sender-constrained tokens
Mutual TLS can bind a client certificate to the token and help prevent token replay.
- Example: service-to-service calls on a private mesh.
- Gotcha: operational complexity is higher than bearer tokens alone.
Rate limits
Rate limits are a contract, not just an error response.
- Example:
429 Too Many RequestswithRetry-After: 60. - Gotcha: tell the client how to back off or you are just forcing guesswork.
Governance
Every public API should have a review path for schema changes, compatibility, and deprecation windows.
- Example: review new enum values and new webhook event types before release.
- Gotcha: additive changes are safer, but only if consumers ignore unknown data gracefully.
Common mistakes and anti-patterns
The failures that make API reviews expensive later.
/doThing, /runJob, and non-HTTP semantics everywhere.