Web Services & APIs — How-to Guides¶
Task-oriented recipes for describing, securing, versioning, testing, documenting, and operating web APIs. Each section answers "how do I do X?" with commands and config you can adapt. For concepts and trade-offs, see Explanation. For versions, status codes, headers, and checklists, see Reference.
Placeholders
Hostnames such as api.example.com, IDs such as 01HXYZ, and variables such as $TOKEN are placeholders. Commands were checked against the tool versions listed in Reference — Tooling Versions.
API Specification Formats¶
Specifications are machine-readable contracts for APIs. They enable codegen, mock servers, linting, and documentation.
OpenAPI 3.1 and 3.2 (REST)¶
OpenAPI is the industry standard for describing HTTP APIs. The latest release is 3.2.1 (2026-09-10, patch of 3.2.0 from 2025-09-19). Version 3.1 aligned the Schema Object with JSON Schema 2020-12. Version 3.2 adds streaming, the QUERY method, and hierarchical tags. Tool support for 3.2 is still catching up in 2026, so check your generator, linter, and docs renderer before you switch the openapi field. Tools ignore the patch number (3.1.0 and 3.1.2 mean the same feature set).
openapi: 3.1.1
info:
title: Orders API
version: 2.4.0
contact:
email: api@example.com
license:
name: Apache 2.0
servers:
- url: https://api.example.com/v2
description: Production
- url: https://sandbox.api.example.com/v2
description: Sandbox
paths:
/orders/{orderId}:
get:
operationId: getOrder
summary: Retrieve a single order
tags: [Orders]
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
responses:
"200":
description: Order found
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
"404":
$ref: "#/components/responses/NotFound"
security:
- bearerAuth: []
components:
schemas:
Order:
type: object
required: [id, status, createdAt]
properties:
id:
type: string
format: uuid
status:
type: string
enum: [pending, confirmed, shipped, delivered, cancelled]
createdAt:
type: string
format: date-time
responses:
NotFound:
description: Resource not found
content:
application/json:
schema:
$ref: "#/components/schemas/ProblemDetail"
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
Key OpenAPI 3.1 changes over 3.0:
- Full JSON Schema 2020-12 alignment (replaces OpenAPI's extended subset). nullable: true is gone; use type: [string, "null"]
- webhooks top-level field for describing webhooks your API sends
- const, examples, and per-schema $schema; jsonSchemaDialect at the root
- exclusiveMinimum/exclusiveMaximum are now numbers (not booleans)
- paths is optional, so a document can hold only components or webhooks
Describe an SSE stream and a QUERY operation (OpenAPI 3.2):
openapi: 3.2.0
info:
title: Orders API
version: 2.5.0
paths:
/orders/events:
get:
summary: Stream order events
responses:
"200":
description: Server-Sent Events stream
content:
text/event-stream:
itemSchema: # applies to each event, not the whole stream
type: object
properties:
event: { type: string }
data: { type: string }
/orders/search:
query: # HTTP QUERY method (RFC 10008)
summary: Search orders with a JSON filter body
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/OrderFilter"
responses:
"200":
description: Matching orders
The full 3.0 / 3.1 / 3.2 feature comparison is in Reference — OpenAPI 3.x Feature Matrix.
AsyncAPI 3.x (Event-Driven APIs)¶
AsyncAPI is the OpenAPI equivalent for WebSocket, MQTT, Kafka, AMQP, and SNS/SQS APIs. The latest release is 3.1.0 (2026-01-31), a non-breaking minor that adds the ROS 2 binding. Bumping asyncapi: 3.0.0 to 3.1.0 requires no other change. Version 3.0 separated channels from operations (send/receive), unlike 2.x's publish/subscribe.
asyncapi: 3.1.0
info:
title: Order Events API
version: 1.0.0
channels:
orderCreated:
address: orders.created
messages:
OrderCreated:
payload:
type: object
properties:
orderId:
type: string
customerId:
type: string
operations:
onOrderCreated:
action: receive
channel:
$ref: "#/channels/orderCreated"
Protocol Buffers IDL (gRPC)¶
See Explanation — Protocol Buffers for the full .proto format. The .proto file IS the API spec for gRPC services.
Validate an AsyncAPI document with the AsyncAPI CLI:
Codegen, mock, lint, and breaking-change tools per format are listed in Reference — Specification Tooling Matrix.
API Gateways¶
An API gateway is the single entry point for all client traffic. It handles routing, auth enforcement, rate limiting, observability, and protocol translation.
flowchart LR
C1[Mobile client] --> GW["API gateway<br/>Kong / Envoy"]
C2[Browser] --> GW
C3[Partner API] --> GW
GW -->|"/v2/orders"| OS[Orders service]
GW -->|"/v2/users"| US[User service]
GW -->|"/v2/products"| PS[Product service]
GW -->|"JWT validation, JWKS"| Auth["OAuth / OIDC server"]
GW -->|"counters"| RL["Rate limit store<br/>Redis"]
GW -->|"OTLP traces, metrics"| Log["Observability<br/>OpenTelemetry Collector"]
Kong Gateway¶
Kong Gateway is built on NGINX + OpenResty (Lua). The open-source edition is Apache-2.0. The public Kong/kong repository's latest changelog entry is 3.9.3 (checked 2026-09). Kong Gateway Enterprise and the Konnect SaaS control plane add RBAC, the Dev Portal, analytics, and advanced plugins such as rate-limiting-advanced.
# Kong declarative config (deck format)
services:
- name: orders-service
url: http://orders-service:8080
plugins:
- name: rate-limiting
config:
minute: 1000
policy: redis
redis:
host: redis # config.redis_host is deprecated since Kong 3.6
- name: jwt
config:
claims_to_verify: [exp]
routes:
- name: orders-route
paths: [/v2/orders]
strip_path: false
methods: [GET, POST, PUT, PATCH, DELETE]
# Sync the declarative file to a running gateway
deck gateway sync kong.yaml
# Kong Admin API — add the correlation-id plugin (generates X-Request-ID per request)
curl -X POST http://kong:8001/routes/orders-route/plugins \
--data name=correlation-id \
--data config.header_name=X-Request-ID \
--data config.generator=uuid
Envoy Proxy¶
Envoy is a high-performance C++ proxy created at Lyft and a CNCF graduated project (2018). It is the data plane of Istio and of Envoy Gateway (the Kubernetes Gateway API implementation). You configure it via xDS APIs (dynamic) or static YAML. The filter below sends rate-limit decisions to an external rate limit service (for example envoyproxy/ratelimit).
# Envoy static config — HTTP rate limit filter
http_filters:
- name: envoy.filters.http.ratelimit
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
domain: orders_api
rate_limit_service:
grpc_service:
envoy_grpc:
cluster_name: rate_limit_service
transport_api_version: V3
AWS API Gateway¶
AWS API Gateway is a managed gateway for REST, HTTP, and WebSocket APIs. It integrates natively with Lambda, ALB, and VPC Link.
# Create HTTP API (simpler, lower cost than REST API)
aws apigatewayv2 create-api \
--name orders-api \
--protocol-type HTTP \
--target arn:aws:lambda:us-east-1:123456789:function:orders-handler
# Add JWT authorizer
aws apigatewayv2 create-authorizer \
--api-id abc123 \
--authorizer-type JWT \
--identity-source '$request.header.Authorization' \
--jwt-configuration Audience=orders-api,Issuer=https://auth.example.com \
--name JwtAuthorizer
A side-by-side gateway comparison (deployment, config model, rate-limit algorithm) is in Reference — API Gateway Comparison.
Authentication and Authorization¶
API Keys¶
API keys are the simplest scheme. They suit server-to-server or developer access where OAuth overhead is unneeded.
Best practices:
- Prefix keys by environment: sk_live_, sk_test_
- Store only the hash (SHA-256) in database — never plaintext
- Rotate on compromise. Provide a 30-day grace period during planned rotations
- Associate keys with scopes: orders:read, orders:write
JWT (JSON Web Tokens)¶
JWTs are stateless bearer tokens. They have three base64url-encoded parts: header, payload, signature.
# Decode the JWT payload without verification (debugging only).
# JWTs use unpadded base64url, so translate the alphabet and re-add padding first.
TOKEN="eyJhbGci..."
p=$(echo "$TOKEN" | cut -d. -f2 | tr '_-' '/+'); while [ $(( ${#p} % 4 )) -ne 0 ]; do p="$p="; done
echo "$p" | base64 -d | jq
// Payload claims
{
"sub": "user_01HXYZ",
"iss": "https://auth.example.com",
"aud": "orders-api",
"exp": 1745600000,
"iat": 1745596400,
"scope": "orders:read orders:write",
"jti": "01HXYZ-unique-token-id"
}
JWT security checklist:
- Use an asymmetric algorithm (ES256, EdDSA, or RS256) so resource servers only need the public key from the JWKS endpoint. Keep HS256 for single-party setups
- Allowlist the expected algorithm on the verifier; reject alg: none (RFC 8725)
- Short expiry: 5–15 minutes for access tokens. Browser apps should keep refresh tokens out of JavaScript (httpOnly cookie via a BFF)
- Validate iss, aud, exp, nbf on every request
- Include jti (JWT ID) for revocation lookup in Redis blocklist
- Never store sensitive data in payload — JWTs are encoded, not encrypted (use JWE for confidentiality)
OAuth 2.0 / OAuth 2.1¶
Authorization Code + PKCE (browser and mobile clients):
sequenceDiagram
participant U as User
participant C as Client App
participant AS as Auth Server
participant RS as Resource Server
C->>C: Generate code_verifier, code_challenge = SHA256(verifier)
C->>AS: GET /authorize?response_type=code&client_id=...&code_challenge=...
AS->>U: Login + Consent screen
U->>AS: Approve
AS->>C: Redirect with ?code=AUTH_CODE
C->>AS: POST /token {code, code_verifier, client_id}
AS->>C: {access_token, refresh_token, expires_in}
C->>RS: GET /orders Authorization: Bearer ACCESS_TOKEN
RS->>C: 200 {orders: [...]}
Client Credentials (machine-to-machine):
curl -X POST https://auth.example.com/oauth/token \
-d grant_type=client_credentials \
-d client_id=service-account \
-d client_secret=secret \
-d scope="orders:read inventory:write"
OAuth 2.1 key changes (still an IETF Internet-Draft; draft-ietf-oauth-v2-1-16, 2026-09-03):
- PKCE required for all clients using the authorization code grant (confidential clients too)
- Implicit grant removed
- Resource Owner Password Credentials (ROPC) grant removed
- Redirect URIs must match exactly (no wildcards)
- Bearer tokens must not be sent in query strings
- Refresh tokens for public clients must be sender-constrained or one-time use (rotation)
You do not need to wait for the RFC: RFC 9700 (OAuth 2.0 Security Best Current Practice, January 2025) already recommends the same profile for OAuth 2.0 deployments.
Sender-constrained tokens with DPoP (RFC 9449) bind an access token to a client key pair, so a stolen token cannot be replayed from another machine:
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
DPoP: eyJ0eXAiOiJkcG9wK2p3dCIsImFsZyI6IkVTMjU2IiwiandrIjp7...
grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA&code_verifier=...
GET /v2/orders HTTP/1.1
Authorization: DPoP <access_token>
DPoP: <new proof JWT signed for this method and URL>
mTLS (Mutual TLS)¶
Both client and server present certificates. This removes shared secrets for service-to-service auth.
# Generate client cert signed by your CA
openssl req -new -key client.key -out client.csr \
-subj "/CN=orders-service/O=internal"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out client.crt -days 365
# Call API with client cert
curl --cert client.crt --key client.key \
--cacert ca.crt \
https://internal-api.example.com/v2/orders
In Kubernetes: use SPIFFE/SPIRE for automatic workload identity, or let Istio inject mTLS transparently via sidecar.
API Versioning¶
Versioning Strategies¶
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI path | /v2/orders |
Most visible, easy routing | Breaks resource identity |
| Query param | /orders?version=2 |
Non-breaking URL | Easily forgotten, cache unfriendly |
| Header | API-Version: 2024-01-01 |
Clean URLs | Less discoverable |
| Content negotiation | Accept: application/vnd.api+json;version=2 |
RFC-compliant | Complex client setup |
URI versioning is the most common choice for public APIs (used by Stripe, Twilio, and others). Stripe combines a coarse URI version (/v1, /v2) with a fine-grained dated header (Stripe-Version: 2026-08-26.dahlia). GitHub's REST API uses a dated header, X-GitHub-Api-Version: 2022-11-28.
Calendar-Based Versioning (Stripe Pattern)¶
Instead of major version bumps, every breaking change ships in a dated version:
Since late 2024 Stripe names versions YYYY-MM-DD.<release>: a named major release (for example acacia, basil, clover, dahlia) carries breaking changes about twice a year, and monthly versions within a release are backward-compatible. A Stripe account is pinned to the version current at its first API request, and requests can override it per call with the header. Server-side, a chain of version "transformers" converts responses from the latest internal shape back to each pinned version.
Deprecation and Sunset Headers (RFC 9745, RFC 8594)¶
HTTP/1.1 200 OK
Deprecation: @1767225600
Sunset: Fri, 01 Jan 2027 00:00:00 GMT
Link: <https://docs.example.com/deprecations/v2>; rel="deprecation"; type="text/html"
Link: <https://docs.example.com/migration/v3>; rel="successor-version"
Deprecation(RFC 9745, March 2025): a Structured Field Date —@plus Unix seconds (@1767225600= 2026-01-01T00:00:00Z). A date in the past means "already deprecated"; a future date announces itSunset(RFC 8594): an HTTP-date after which the resource may stop responding. It must not be earlier than theDeprecationdateLinkwithrel="deprecation"(RFC 9745) points to human-readable deprecation notes.rel="successor-version"points to the replacement- After the sunset date, return
410 Gonewith a Problem Details body that links to the migration guide
Non-Breaking vs Breaking Changes¶
Non-breaking (safe to ship): - Adding optional request fields - Adding new response fields - Adding new endpoints - New enum values (unless clients use exhaustive matching)
Breaking (require new version): - Removing or renaming fields - Changing field types - Changing HTTP method for an operation - Altering authentication requirements - Removing enum values
Rate Limiting¶
Rate limiting protects services from abuse. It enforces fair usage and supports monetization tiers.
Algorithms¶
Token Bucket (allow bursting):
capacity = 100 tokens
refill_rate = 10 tokens/second
on request:
if tokens >= cost:
tokens -= cost
return ALLOW
else:
return 429 Too Many Requests
AWS API Gateway throttles with a token bucket (rate = refill, burst = bucket size). Kong's open-source rate-limiting plugin uses fixed-window counters per second/minute/hour/day. The Enterprise rate-limiting-advanced plugin adds sliding windows. NGINX limit_req is a leaky bucket.
Sliding Window Log (most precise):
This algorithm stores the timestamp of each request. It counts requests within [now - window, now]. Memory cost is high at scale.
Sliding Window Counter (approximation, low memory):
Redis-based implementation: two counters (current window, previous window) per key.
Fixed Window (simplest, boundary spike risk):
This algorithm resets the counter at fixed intervals. A burst at 11:59:59 and 12:00:01 yields 2× the allowed rate.
Response Headers¶
Most APIs today send the de facto X-RateLimit-* headers. The IETF RateLimit-Policy / RateLimit header fields are still an Internet-Draft (draft-ietf-httpapi-ratelimit-headers-11, May 2026) and their syntax has changed between drafts, so treat them as optional extras, not a replacement. Retry-After (RFC 9110) is the one standard header every client should honor.
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 847
X-RateLimit-Reset: 1745600000
Retry-After: 30
On 429:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1745600000
Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/rate-limit-exceeded",
"title": "Too Many Requests",
"status": 429,
"detail": "You have exceeded 1000 requests per minute."
}
Rate Limit Keys¶
Choose the right granularity:
| Key | Use Case |
|---|---|
| IP address | Unauthenticated public APIs, DDoS protection |
| API key | Developer tier enforcement |
| User ID | Per-account limits after auth |
| Endpoint | Expensive operations (for example, /search) |
| Tenant ID | SaaS multi-tenant isolation |
CORS (Cross-Origin Resource Sharing)¶
CORS restricts which browser origins can call your API. It does NOT protect server-to-server calls.
# Preflight request (browser auto-sends for non-simple requests)
OPTIONS /v2/orders HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
# Server response
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
Critical rules:
- Never set Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers reject the response. Echo the specific allowed origin and add Vary: Origin
- Never reflect the request Origin header without checking it against the allowlist; that is equivalent to * with credentials
- Maintain an allowlist of trusted origins. Validate dynamically against it
- Cache preflight with Access-Control-Max-Age to reduce OPTIONS overhead
API Design Best Practices¶
Resource Naming¶
# Good — noun-based, plural, lowercase
GET /v2/orders
POST /v2/orders
GET /v2/orders/{orderId}
PUT /v2/orders/{orderId}
PATCH /v2/orders/{orderId}
DELETE /v2/orders/{orderId}
# Nested resources — use sparingly; max 2 levels deep
GET /v2/orders/{orderId}/items
POST /v2/orders/{orderId}/items
# Actions (verbs) — use only for operations that don't map to CRUD
POST /v2/orders/{orderId}/cancel
POST /v2/orders/{orderId}/refund
POST /v2/payments/{paymentId}/capture
Idempotency Keys¶
When clients retry on network failure, prevent duplicate processing.
POST /v2/orders HTTP/1.1
Idempotency-Key: 01HXYZ-unique-request-id
Content-Type: application/json
{"productId": "prod_123", "quantity": 2}
Server logic:
1. Hash Idempotency-Key → look up in idempotency store (Redis/DB)
2. If found and result cached → return cached response immediately
3. If found and in-flight → return 409 Conflict or wait
4. If not found → process, store result keyed to hash, return result
TTL: 24–48 hours (Stripe prunes keys after at least 24 hours)
Also store a fingerprint of the request body with the key. If a client reuses a key with a different body, return 422 instead of replaying the old response. The Idempotency-Key header is being standardized in draft-ietf-httpapi-idempotency-key-header (Internet-Draft, latest -07, 2025-10).
Pagination¶
Cursor-based (recommended for large/real-time datasets):
// Request: GET /v2/orders?limit=20&after=01HXYZ
{
"data": [...],
"pagination": {
"limit": 20,
"hasNextPage": true,
"nextCursor": "01HABC",
"hasPrevPage": true,
"prevCursor": "01HWXY"
}
}
Offset-based (simpler, do not use it for real-time data — page drift on writes):
// Request: GET /v2/orders?limit=20&offset=40
{
"data": [...],
"pagination": {
"total": 1847,
"limit": 20,
"offset": 40,
"pages": 93
}
}
Standardized Error Responses (RFC 9457 / Problem Details)¶
{
"type": "https://api.example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "Request body contains invalid fields.",
"instance": "/v2/orders/01HXYZ",
"errors": [
{
"field": "quantity",
"message": "Must be a positive integer",
"code": "INVALID_VALUE"
},
{
"field": "productId",
"message": "Product not found",
"code": "RESOURCE_NOT_FOUND"
}
],
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}
Always include traceId or requestId for support/debugging correlation.
Long-Running Operations (202 Async Pattern)¶
# 1. Client submits job
POST /v2/reports HTTP/1.1
{"type": "monthly-revenue", "month": "2026-03"}
# 2. Server accepts immediately
HTTP/1.1 202 Accepted
Location: /v2/reports/jobs/job_01HXYZ
Retry-After: 30
# 3. Client polls
GET /v2/reports/jobs/job_01HXYZ
# 4a. Still processing
HTTP/1.1 200 OK
{"status": "processing", "progress": 42, "estimatedCompletion": "2026-04-25T10:15:00Z"}
# 4b. Complete
HTTP/1.1 200 OK
{"status": "complete", "resultUrl": "/v2/reports/rpt_01HABC", "expiresAt": "2026-04-26T10:00:00Z"}
# 5. Retrieve result
GET /v2/reports/rpt_01HABC
Alternative: use webhook callback instead of polling — POST /v2/reports body includes callbackUrl.
Filtering, Sorting, Searching¶
# Filtering — use query params
GET /v2/orders?status=pending&customerId=cust_123&createdAfter=2026-01-01
# Sorting — field and direction
GET /v2/orders?sort=-createdAt,+status # minus = desc, plus = asc
# Sparse fieldsets — reduce payload size
GET /v2/orders?fields=id,status,total
# Full-text search
GET /v2/products?q=wireless+headphones&category=electronics
API First Design¶
Design the API contract before writing implementation code.
Workflow:
1. Write OpenAPI spec in YAML (use Spectral to lint against rules)
2. Generate mock server with Prism: prism mock openapi.yaml
3. Share mock URL with frontend team — both sides develop in parallel
4. Generate server stubs with oapi-codegen (Go), openapi-generator (Java, Python, and more)
5. Write implementation against generated interfaces
6. Run contract tests against live server to verify spec compliance
# Prism mock server (read OpenAPI spec, serve mock responses)
npx @stoplight/prism-cli mock openapi.yaml --port 4010
# Call mock
curl http://localhost:4010/v2/orders/01HXYZ \
-H "Authorization: Bearer test-token"
# Prism validation proxy (forward to real server, validate request/response against spec)
npx @stoplight/prism-cli proxy openapi.yaml http://localhost:8080
Testing¶
REST API Testing (curl)¶
# GET with auth header and pretty JSON
curl -s -X GET https://api.example.com/v2/orders/01HXYZ \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: application/json" | jq
# POST with JSON body
curl -s -X POST https://api.example.com/v2/orders \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"productId": "prod_123", "quantity": 2}' | jq
# Test rate limiting — fire 10 requests rapidly
for i in {1..10}; do
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Authorization: Bearer $TOKEN" \
https://api.example.com/v2/orders
done
# Inspect headers only
curl -sI https://api.example.com/v2/orders
# Follow redirects, show timing breakdown
curl -s -o /dev/null -L \
-w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
https://api.example.com/v2/orders
# Force HTTP/3 (needs a curl build with HTTP/3 support; check `curl -V` for HTTP3)
curl -sI --http3-only https://api.example.com/v2/orders
gRPC Testing (grpcurl)¶
# Install (or: go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest)
brew install grpcurl
# List services (server reflection must be enabled)
grpcurl -plaintext localhost:50051 list
# Describe a service
grpcurl -plaintext localhost:50051 describe orders.OrderService
# Unary call
grpcurl -plaintext \
-H "Authorization: Bearer $TOKEN" \
-d '{"order_id": "01HXYZ"}' \
localhost:50051 orders.OrderService/GetOrder
# Server streaming call
grpcurl -plaintext \
-d '{"customer_id": "cust_123"}' \
localhost:50051 orders.OrderService/WatchOrders
# Call with mTLS (flags must come before the address)
grpcurl \
-cert client.crt -key client.key -cacert ca.crt \
-d '{"order_id": "01HXYZ"}' \
api.example.com:443 orders.OrderService/GetOrder
# Health check (grpc.health.v1): overall server, then one service
grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check
grpcurl -plaintext -d '{"service": "orders.OrderService"}' \
localhost:50051 grpc.health.v1.Health/Check
# Auth checks: no token, invalid token, valid token
grpcurl -plaintext localhost:50051 orders.OrderService/GetOrder
grpcurl -plaintext -H "authorization: Bearer invalid_token" \
localhost:50051 orders.OrderService/GetOrder
grpcurl -plaintext -H "authorization: Bearer $TOKEN" \
-d '{"order_id": "01HXYZ"}' localhost:50051 orders.OrderService/GetOrder
Expect Unauthenticated (code 16) for the first two calls. Status code meanings are in Reference — gRPC Status Codes.
Kubernetes gRPC probe (stable since Kubernetes v1.27; the container must serve grpc.health.v1.Health):
livenessProbe:
grpc:
port: 50051
service: "" # empty = overall server health
initialDelaySeconds: 10
periodSeconds: 10
readinessProbe:
grpc:
port: 50051
service: orders.OrderService
WebSocket Testing (wscat)¶
# Install
npm install -g wscat
# Connect to WebSocket server
wscat -c wss://api.example.com/ws \
--header "Authorization: Bearer $TOKEN"
# Send a message (after connecting)
> {"type": "subscribe", "channel": "orders", "customerId": "cust_123"}
< {"type": "subscribed", "channel": "orders"}
< {"type": "order.updated", "orderId": "01HXYZ", "status": "shipped"}
# Connect with subprotocol
wscat -c wss://api.example.com/ws --subprotocol "v2.orders"
GraphQL Testing (curl + jq)¶
# Introspection query
curl -s -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { types { name } } }"}' | jq '.data.__schema.types[].name'
# Query with variables
curl -s -X POST https://api.example.com/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "query GetOrder($id: ID!) { order(id: $id) { status total } }",
"variables": {"id": "01HXYZ"}
}' | jq
# Mutation
curl -s -X POST https://api.example.com/graphql \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"query": "mutation CancelOrder($id: ID!) { cancelOrder(id: $id) { success } }",
"variables": {"id": "01HXYZ"}
}' | jq
Load Testing (k6)¶
// k6 load test script — orders API
import http from "k6/http";
import { check, sleep } from "k6";
import { Rate } from "k6/metrics";
const errorRate = new Rate("errors");
export const options = {
stages: [
{ duration: "30s", target: 50 }, // ramp up to 50 VUs
{ duration: "2m", target: 50 }, // hold
{ duration: "30s", target: 200 }, // spike to 200 VUs
{ duration: "1m", target: 200 }, // hold spike
{ duration: "30s", target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ["p(95)<500"], // 95th percentile < 500ms
errors: ["rate<0.01"], // error rate < 1%
},
};
export default function () {
const res = http.get("https://api.example.com/v2/orders", {
headers: { Authorization: `Bearer ${__ENV.API_TOKEN}` },
});
const ok = check(res, {
"status is 200": (r) => r.status === 200,
"response time < 500ms": (r) => r.timings.duration < 500,
});
errorRate.add(!ok);
sleep(1);
}
Contract Testing (Pact)¶
Consumer-driven contract tests verify that API providers honour the contracts that consumers expect.
# Consumer tests (Pact JS, pytest-pact, ...) write pact files to ./pacts
# CLI tools ship in @pact-foundation/pact-cli (pact-broker, pact-verifier, ...)
npm install -D @pact-foundation/pact-cli
# Publish consumer pacts to the Pact Broker / PactFlow
npx pact-broker publish ./pacts \
--broker-base-url https://your-pact-broker.example.com \
--consumer-app-version "$(git rev-parse HEAD)" \
--branch "$(git rev-parse --abbrev-ref HEAD)"
# Provider side: verify all pacts for this provider against a running service
npx pact-verifier \
--hostname localhost --port 8080 \
--broker-url https://your-pact-broker.example.com \
--provider-name orders-service \
--publish --provider-version "$(git rev-parse HEAD)"
Monitoring and Observability¶
Key Metrics (RED Method)¶
| Metric | Description | Alert Threshold (example) |
|---|---|---|
| Rate | Requests per second | Traffic drop > 50% vs baseline |
| Errors | 5xx error rate | > 1% over 5 minutes |
| Duration | p50, p95, p99 latency | p99 > 1000ms |
Additional API-specific metrics:
- 4xx rate (client errors) — a spike can indicate a breaking change or client bug
- Auth failure rate — spike indicates credential attack or misconfiguration
- Rate limit hit rate (429 responses) — indicate capacity planning needs
- Payload size distribution — detect runaway requests
Distributed Tracing (OpenTelemetry)¶
# Node.js — auto-instrumentation with OTLP export
npm install @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node
# Inject trace context headers
GET /v2/orders HTTP/1.1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
tracestate: rend=congo
Propagate traceparent across all service boundaries. Every response must include X-Request-ID or X-Trace-ID tied to the trace.
Structured Logging¶
{
"level": "info",
"timestamp": "2026-04-25T10:00:00.123Z",
"service": "orders-api",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"method": "GET",
"path": "/v2/orders/01HXYZ",
"statusCode": 200,
"durationMs": 47,
"customerId": "cust_123",
"region": "us-east-1"
}
Health Endpoints¶
# Liveness — is the process alive?
GET /health/live
HTTP/1.1 200 OK
{"status": "ok"}
# Readiness — is the service ready to receive traffic?
GET /health/ready
HTTP/1.1 200 OK
{
"status": "ok",
"checks": {
"database": "ok",
"cache": "ok",
"dependencyServiceA": "ok"
}
}
# Degraded state
HTTP/1.1 503 Service Unavailable
{
"status": "degraded",
"checks": {
"database": "ok",
"cache": "error",
"dependencyServiceA": "ok"
}
}
Circuit Breaker Pattern¶
The circuit breaker prevents cascading failures when a downstream dependency is degraded.
The state machine below uses example thresholds. Tune them per dependency.
stateDiagram-v2
[*] --> Closed
Closed --> Open: failure rate over 50% in last 10 calls
Open --> HalfOpen: cooldown elapsed (for example 30 s)
HalfOpen --> Closed: 3 consecutive probe successes
HalfOpen --> Open: any probe failure
note right of Open
Calls fail fast (503 or cached fallback)
without touching the dependency
end note
Libraries: Resilience4j (Java), Polly (.NET), opossum (Node.js), gobreaker (Go). Service meshes provide a coarser version without code changes: Envoy/Istio outlier detection ejects failing endpoints from the load-balancing pool.
Webhooks as a Product¶
For APIs that offer webhooks, treat the delivery system as a first-class product. Why delivery is at-least-once and how signatures prevent forgery is explained in Explanation — Webhooks. The Standard Webhooks spec (headers webhook-id, webhook-timestamp, webhook-signature) is a good default wire format; the Python below uses the similar Stripe-style t=...,v1=... header.
Delivery Architecture¶
sequenceDiagram
participant ES as Event Source
participant Q as Message Queue
participant WD as Webhook Dispatcher
participant C as Customer Server
ES->>Q: Publish event
Q->>WD: Consume event
WD->>C: POST /webhook (signed payload)
alt Success (2xx)
C->>WD: 200 OK (within 5s)
WD->>Q: Ack message
else Failure / Timeout
WD->>Q: Nack / retry
WD->>WD: Exponential backoff (5s, 25s, 125s, ...)
WD->>WD: After 72h: mark dead, alert
end
Payload Signing (HMAC-SHA256)¶
import hashlib, hmac, time
def sign_payload(secret: str, payload: bytes) -> str:
timestamp = str(int(time.time()))
message = f"{timestamp}.{payload.decode()}".encode()
signature = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return f"t={timestamp},v1={signature}"
def verify_signature(secret: str, payload: bytes, header: str, tolerance: int = 300) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
timestamp = int(parts["t"])
if abs(time.time() - timestamp) > tolerance:
return False # replay attack
message = f"{timestamp}.{payload.decode()}".encode()
expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts["v1"])
Reliability Patterns¶
| Pattern | Implementation |
|---|---|
| Idempotency keys | Include webhookId in payload. The consumer deduplicates |
| Immediate 200 | Return 200 before processing. Use a queue for async work |
| Retry with backoff | 5s → 25s → 125s → 625s. Max 72h delivery window |
| Dead letter queue | After max retries, route to DLQ. Alert the operator |
| Event ordering | Include sequence counter. The consumer handles out-of-order delivery |
| Event replay | Let consumers re-request past events by ID or time range after an outage |
| CloudEvents format | Standardize payload envelope (specversion, type, source, id) |
Webhook Management Portal (product features)¶
- Endpoint registration with per-event-type subscription
- Delivery attempt log with request/response bodies (last 30 days)
- Manual replay of failed deliveries
- HMAC secret rotation (grace period supporting both old + new key)
200 OKwebhook test endpoint for validation
API Caching Strategies¶
Caching is the single most impactful API performance optimization. Multiple layers can cache independently.
Caching Layers¶
flowchart LR
C[Client] -->|1| BC["Browser cache<br/>Cache-Control"]
BC -->|2| CDN["CDN edge<br/>Cloudflare / CloudFront / Fastly"]
CDN -->|3| GW["Gateway / reverse proxy<br/>Varnish / NGINX"]
GW -->|4| APP["Application cache<br/>Redis / Memcached"]
APP -->|5| DB[("Database<br/>buffer cache")]
Cache-Control Patterns¶
# Immutable asset (hashed filename — never changes)
Cache-Control: public, max-age=31536000, immutable
# Frequently changing API resource
Cache-Control: private, max-age=0, must-revalidate
ETag: "a1b2c3"
# Shared resource (CDN-cacheable)
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
Vary: Accept-Encoding, Authorization
# No caching (sensitive data)
Cache-Control: no-store
stale-while-revalidate — the CDN/proxy serves the stale cached response immediately while fetching a fresh copy in the background. The client gets a fast response. The cache updates asynchronously. This matters for APIs where slight staleness is permitted (product catalogs, search results).
stale-if-error — serve stale content if the origin returns a 5xx error. This gives graceful degradation when the backend is down.
Cache Invalidation Patterns¶
| Pattern | How It Works | Best For |
|---|---|---|
| TTL expiry | Cache expires after max-age seconds |
Simple and predictable. Permitted staleness |
| Event-driven purge | Backend publishes event → CDN/cache purge API called | Real-time consistency. More infrastructure |
| Surrogate keys (tags) | Tag cached responses. Purge all responses with a tag | Purge all /products/* when inventory changes |
| Conditional revalidation | If-None-Match / If-Modified-Since → 304 or fresh |
Bandwidth savings. The origin is still hit |
# Fastly — purge by surrogate key
curl -X POST https://api.fastly.com/service/SERVICE_ID/purge/product-42 \
-H "Fastly-Key: $FASTLY_TOKEN"
# CloudFront — invalidation
aws cloudfront create-invalidation \
--distribution-id E1234 \
--paths "/v2/products/42" "/v2/products?category=electronics"
GraphQL Caching¶
The GraphQL POST /graphql endpoint breaks traditional HTTP caching — see apis/web-services/explanation#caching for normalized client cache, APQ, and @cacheControl directive approaches.
Retry Patterns¶
Retries are essential for resilient API consumers, but naive retries cause retry storms that amplify failures.
Exponential Backoff with Jitter¶
Attempt 1: wait 0ms (immediate)
Attempt 2: wait random(0, 1000ms) → e.g., 487ms
Attempt 3: wait random(0, 2000ms) → e.g., 1,342ms
Attempt 4: wait random(0, 4000ms) → e.g., 2,891ms
Attempt 5: wait random(0, 8000ms) → e.g., 5,203ms
Give up after attempt 5
Full jitter (recommended by AWS) prevents thundering herd — all retrying clients spread randomly across the backoff window instead of hitting the server at the same instant.
import random, time
def retry_with_backoff(func, max_retries=5, base_delay=1.0, max_delay=30.0):
for attempt in range(max_retries):
try:
return func()
except RetryableError:
if attempt == max_retries - 1:
raise
delay = min(base_delay * (2 ** attempt), max_delay)
jittered = random.uniform(0, delay) # full jitter
time.sleep(jittered)
Retry Budgets¶
Instead of per-request retry limits, set a budget: "retry at most 10% of total requests." This prevents cascading retry storms where every client retries simultaneously during an outage.
If 1000 req/s normally, allow at most 100 retries/s total
When budget exhausted → fail fast instead of retrying
Envoy supports this natively (circuit_breakers.thresholds.retry_budget on a cluster, with budget_percent and min_retry_concurrency). Istio exposes it as trafficPolicy.retryBudget in a DestinationRule (default 20% of active requests, minimum 3 concurrent retries). Linkerd has retry budgets on its retry configuration as well.
Which Errors to Retry¶
| Status Code | Retry? | Reason |
|---|---|---|
408 Request Timeout |
Yes | Transient timeout |
429 Too Many Requests |
Yes (respect Retry-After) |
Rate limited. Wait and retry |
500 Internal Server Error |
Yes (cautiously) | Can be transient. Limit retries |
502 Bad Gateway |
Yes | Upstream briefly unavailable |
503 Service Unavailable |
Yes (respect Retry-After) |
Server overloaded. Back off |
504 Gateway Timeout |
Yes | Upstream timeout |
400 Bad Request |
No | Client error. Retry does not help |
401/403 |
No | Auth issue. A retry with the same credentials does not help |
404 |
No | Resource does not exist |
409 Conflict |
Maybe | Re-read state, then maybe retry with updated data |
422 |
No | Validation error. Fix the input first |
Idempotency Requirement
Only retry non-idempotent operations (POST) if the API supports idempotency keys. Otherwise, a retried POST can create duplicate resources.
SDK and Client Code Generation¶
Generating typed client SDKs from API specifications eliminates hand-written HTTP calls and catches breaking changes at compile time.
REST — openapi-generator¶
# Install
npm install -g @openapitools/openapi-generator-cli
# Generate TypeScript client from OpenAPI spec
openapi-generator-cli generate \
-i https://api.example.com/v2/openapi.yaml \
-g typescript-fetch \
-o ./generated/api-client \
--additional-properties=supportsES6=true,npmName=@example/api-client
# Generate Go server stubs
openapi-generator-cli generate \
-i openapi.yaml \
-g go-server \
-o ./internal/api
TypeScript-only alternatives that generate lighter output: openapi-typescript (types only, pairs with openapi-fetch) and @hey-api/openapi-ts (types + SDK).
oapi-codegen (Go-specific, lighter weight):
# Generate Go types + Echo server from spec
oapi-codegen -package api -generate types,server openapi.yaml > api/api.gen.go
Generated client usage (TypeScript):
import { OrdersApi, Configuration } from '@example/api-client';
const api = new OrdersApi(new Configuration({
basePath: 'https://api.example.com/v2',
accessToken: token,
}));
// Fully typed — input and output types from OpenAPI spec
const order = await api.getOrder({ orderId: '01HXYZ' });
// order is typed as Order, not `any`
GraphQL — graphql-codegen¶
npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
@graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'https://api.example.com/graphql',
documents: 'src/**/*.graphql',
generates: {
'./src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typed-document-node',
],
},
},
};
export default config;
Result: every .graphql query/mutation file produces a fully typed TypedDocumentNode — input variables and response shape are both type-checked at compile time. For React and other frontends, the GraphQL Code Generator docs recommend @graphql-codegen/client-preset, which generates a typed graphql() function for operations written inline in components.
gRPC — buf generate¶
# buf.gen.yaml — code generation config
version: v2
plugins:
- remote: buf.build/protocolbuffers/go
out: gen/go
opt: paths=source_relative
- remote: buf.build/grpc/go
out: gen/go
opt: paths=source_relative
- remote: buf.build/connectrpc/go
out: gen/go
opt: paths=source_relative
buf advantages over raw protoc:
- Dependency management (BSR — Buf Schema Registry)
- buf lint — enforces proto style guide
- buf breaking — detects breaking changes between proto versions in CI
- buf generate — replaces complex protoc plugin chains
API Documentation Generation¶
Swagger UI¶
Swagger UI is interactive documentation from an OpenAPI spec. Users can try API calls directly in the browser.
# Docker — serve Swagger UI for your spec
docker run -p 8080:8080 \
-e SWAGGER_JSON=/spec/openapi.yaml \
-v $(pwd):/spec \
swaggerapi/swagger-ui
# Or embed in Express:
npm install swagger-ui-express
const swaggerUi = require('swagger-ui-express');
const spec = require('./openapi.json');
app.use('/docs', swaggerUi.serve, swaggerUi.setup(spec));
Redoc¶
Redoc produces static, clean, three-panel documentation. It is better for public-facing API docs than Swagger UI.
# CLI rendering
npx @redocly/cli build-docs openapi.yaml -o docs/index.html
# Or CDN-hosted single HTML
# <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
# <redoc spec-url="openapi.yaml"></redoc>
Scalar¶
Scalar is a modern, customizable API reference with a built-in API client. It is a growing alternative to Swagger UI.
// Express integration
import { apiReference } from '@scalar/express-api-reference';
app.use('/reference', apiReference({
url: '/openapi.json', // older releases used spec: { url }
theme: 'purple',
}));
GraphQL Documentation¶
- GraphiQL — official in-browser IDE with docs explorer, query autocomplete, variable pane
- Apollo Studio / Apollo Sandbox — schema explorer, operation history, field usage analytics
- Postman — supports GraphQL schema import and introspection
Most servers embed an explorer, so there is nothing extra to install: GraphQL Yoga serves GraphiQL on its /graphql endpoint, and Apollo Server serves the Apollo Sandbox landing page in development. To host GraphiQL yourself, use the graphiql React package (v5.x, 2026-09) in a small web app; it has no CLI.
API Governance and Linting¶
Spectral (OpenAPI / AsyncAPI Linting)¶
Spectral enforces API design standards via configurable rules. Run in CI to prevent non-compliant changes.
# Install
npm install -g @stoplight/spectral-cli
# Lint against built-in OpenAPI rules
spectral lint openapi.yaml
# Lint against custom ruleset
spectral lint openapi.yaml --ruleset .spectral.yaml
# .spectral.yaml — custom API governance rules
extends:
- spectral:oas
rules:
# Require operationId on every endpoint
operation-operationId:
severity: error
given: "$.paths[*][*]"
then:
field: operationId
function: truthy
# Enforce kebab-case paths (the ~ selects each path key, not its value)
paths-kebab-case:
severity: error
given: "$.paths[*]~"
then:
function: pattern
functionOptions:
match: "^(/([a-z0-9-]+|\\{[a-zA-Z0-9_]+\\}))+$"
# Require description on all parameters
parameter-description:
severity: warn
given: "$.paths[*][*].parameters[*]"
then:
field: description
function: truthy
# Ban query string versioning
no-query-version:
severity: error
given: "$.paths[*][*].parameters[?(@.name == 'version' && @.in == 'query')]"
then:
function: falsy
# Require error response schemas
require-error-responses:
severity: warn
given: "$.paths[*][*].responses"
then:
- field: "400"
function: truthy
- field: "500"
function: truthy
Breaking Change Detection¶
# oasdiff — breaking-change report between two OpenAPI documents (files or URLs)
# install: brew install oasdiff (or: go install github.com/oasdiff/oasdiff@latest)
oasdiff breaking old-spec.yaml new-spec.yaml
oasdiff breaking old-spec.yaml new-spec.yaml --fail-on ERR # exit 1 on breaking changes
# buf breaking — detect protobuf breaking changes
buf breaking proto/ --against '.git#branch=main'
# optic — track API changes in CI (low release activity since 2025)
npx @useoptic/optic diff openapi.yaml --base main --check
CI integration example (GitHub Actions):
- name: Lint API spec
run: npx @stoplight/spectral-cli lint openapi.yaml --fail-severity warn
- name: Check for breaking changes
run: |
git show origin/main:openapi.yaml > "$RUNNER_TEMP/base.yaml"
oasdiff breaking "$RUNNER_TEMP/base.yaml" openapi.yaml --fail-on ERR
Proto Linting with buf¶
# buf.yaml — proto lint configuration
version: v2
lint:
use:
- STANDARD # Google's protobuf style guide
- COMMENTS # Require comments on all public types
except:
- PACKAGE_VERSION_SUFFIX
# Run lint
buf lint proto/
# Check for breaking changes against main branch
buf breaking proto/ --against '.git#branch=main'
buf breaking catches: field number reuse, type changes, field removal, service method signature changes — all before merge.
API Security Testing¶
Run these checks against a staging environment with two test accounts (user A and user B) and one admin account. Every command should be rejected; a 2xx response is a finding. Why each attack works is covered in Explanation — OWASP API Security Top 10. Scanner options are in Reference — Security Testing Tools.
Security Testing Checklist¶
# 1. Test BOLA — access another user's resource with your token (expect 403/404)
curl -H "Authorization: Bearer USER_A_TOKEN" \
https://api.example.com/v2/orders/USER_B_ORDER_ID
# 2. Test BFLA — call admin endpoint with regular user token (expect 403)
curl -X DELETE -H "Authorization: Bearer REGULAR_TOKEN" \
https://api.example.com/v2/admin/users/user_456
# 3. Test mass assignment — send privileged fields (expect 400/422, or fields ignored)
curl -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"role": "admin", "isAdmin": true}' \
https://api.example.com/v2/users/me
# 4. Test rate limiting — burst requests (expect 429 after the limit)
for i in {1..100}; do
curl -s -o /dev/null -w "%{http_code}\n" \
-H "Content-Type: application/json" \
-d '{"email":"test@test.com","password":"wrong"}' \
https://api.example.com/v2/auth/login
done | sort | uniq -c
# 5. Test SSRF — webhook URL pointing to cloud metadata (expect 400/422)
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "http://169.254.169.254/latest/meta-data/"}' \
https://api.example.com/v2/webhooks
# 6. Test excessive data — request huge page (expect capped page size or 400)
curl -H "Authorization: Bearer $TOKEN" "https://api.example.com/v2/users?limit=999999"
# 7. Test GraphQL introspection in production (expect an error)
curl -X POST https://api.example.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { types { name } } }"}'
# 8. Test JWT alg:none — unsigned token (expect 401)
b64url() { base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n'; }
JWT="$(printf '{"alg":"none","typ":"JWT"}' | b64url).$(printf '{"sub":"admin","role":"admin"}' | b64url)."
curl -H "Authorization: Bearer $JWT" https://api.example.com/v2/users/me
Automate the same checks in CI with a schema-driven fuzzer:
# Schemathesis: property-based tests generated from the OpenAPI document
uvx schemathesis run https://staging.api.example.com/openapi.json \
--header "Authorization: Bearer $TOKEN"
Transport Security¶
TLS Configuration¶
A baseline NGINX server block based on the Mozilla "intermediate" profile. Regenerate it with the Mozilla SSL Configuration Generator for your NGINX and OpenSSL versions. The full checklist is in Reference — TLS Hardening Checklist.
# NGINX — TLS 1.2 + 1.3 (Mozilla intermediate)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# OCSP stapling only helps with CAs that still run OCSP (Let's Encrypt stopped in Aug 2025)
# ssl_stapling on;
# ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
Verify the result:
# Protocol and cipher negotiated
openssl s_client -connect api.example.com:443 -servername api.example.com -tls1_3 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"
# Legacy protocols must fail
openssl s_client -connect api.example.com:443 -tls1_1 </dev/null
Get the SPKI pin hash for certificate pinning (see Explanation — Certificate Pinning for the trade-offs):
openssl x509 -in server.crt -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | base64
API Tooling Ecosystem¶
The categorized tool list (editors, linters, mocks, clients, load and contract testing, documentation, gateways, meshes, codegen, monitoring) is in Reference — API Tooling Ecosystem.
Sources¶
OpenAPI & Specification¶
- OpenAPI Specification v3.2.1 and v3.1.1
- AsyncAPI 3.1.0 specification
- Spectral OpenAPI Linting — Stoplight
- Azure REST API Guidelines — Microsoft (the older root
Guidelines.mdis deprecated) - Google API Design Guide
- Zalando RESTful API Guidelines
Authentication & Security¶
- OAuth 2.0 — RFC 6749
- OAuth 2.1 Draft
- OAuth 2.0 Security Best Current Practice — RFC 9700
- DPoP — RFC 9449
- PKCE — RFC 7636
- JSON Web Tokens — RFC 7519
- JWT Best Current Practices — RFC 8725
- Mozilla SSL Configuration Generator
- OWASP API Security Top 10 (2023)
- SPIFFE/SPIRE — Workload Identity
API Design¶
- Idempotency Keys — Stripe Docs
- HTTP Problem Details — RFC 9457
- Sunset Header — RFC 8594
- Deprecation Header — RFC 9745
- Idempotency-Key header draft
- RateLimit header fields draft
- Stripe API versioning
- Cursor Pagination — Slack Engineering
- API Versioning — Stripe Blog
Rate Limiting & Gateways¶
- Kong Gateway Documentation
- Istio DestinationRule reference (retryBudget)
- Envoy Proxy — Rate Limiting
- AWS API Gateway Documentation
- Rate Limiting Algorithms — Stripe Engineering
Webhooks¶
Testing & Monitoring¶
- Grafana k6 Documentation
- grpcurl GitHub
- Pact Contract Testing and pact-js-cli
- Schemathesis
- Kubernetes gRPC probes
- OpenTelemetry Documentation
- Prism Mock Server — Stoplight