Technical Interview Guide: Welcome to this comprehensive, in-depth preparation guide featuring curated, real-world, and scenario-based Web API and RESTful architecture technical interview questions and answers. Designed specifically for senior backend engineers, API designers, and enterprise software architects, this guide covers architectural constraints, HTTP semantics, idempotency, content negotiation, API gateways, rate limiting, security, caching, and production resilience.
Web API & REST Guide
Topics covered: Richardson Maturity Model, Architectural Constraints, Safe vs Idempotent Methods, HTTP/1.1 vs HTTP/2 vs HTTP/3, Status Code Semantics, Versioning Schemes, HATEOAS, RFC 9457 Problem Details, OAuth 2.1 Mutual TLS/DPoP, Token Bucket Rate Limiting, Conditional Requests (ETags, 304 Not Modified), API Gateways, and Webhook Delivery Guarantees.
REST Architectural Constraints & Maturity Model
REST (Representational State Transfer) is an architectural style for distributed hypermedia systems. To be considered truly RESTful, a service must adhere to 6 architectural constraints:
- Client-Server: Enforces clean separation of concerns. The client manages user interface state, while the server handles data storage and domain logic.
- Stateless: Every request from client to server must contain all information required to understand and process the request. No client context is stored on the server between requests.
- Cacheable: Responses must explicitly declare themselves as cacheable or non-cacheable to prevent clients from reusing stale data.
- Uniform Interface: Simplifies and decouples architecture via 4 sub-constraints: resource identification (URIs), manipulation through representations, self-descriptive messages, and hypermedia (HATEOAS).
- Layered System: The client cannot tell whether it is connected directly to the end server or to an intermediary (proxy, cache, API gateway, load balancer).
- Code on Demand (Optional): Servers can temporarily extend client functionality by transferring executable code (e.g., JavaScript).
The Richardson Maturity Model (RMM) grades an API's adherence to REST principles:
- Level 0 (The Swamp of POX): Uses HTTP purely as a transport tunnel. A single URI receives all requests (typically via
POST) with custom XML/JSON envelopes (e.g., SOAP or basic XML-RPC). - Level 1 (Resources): Introduces multiple discrete URIs to represent individual resources (e.g.,
/orders/123), but still relies on a single HTTP verb (usuallyPOST). - Level 2 (HTTP Verbs): Uses standard HTTP verbs (
GET,POST,PUT,DELETE,PATCH) according to their intended semantics, and returns appropriate standard HTTP status codes (200, 201, 404, 500). Most commercial "REST" APIs sit at Level 2. - Level 3 (Hypermedia Controls / HATEOAS): Responses include hypermedia links that guide the client on what actions and transitions can be performed next based on the current resource state.
Statelessness requires that the server never stores session state across requests. Every incoming request must contain authentication credentials, authorization tokens, and all necessary state parameters.
Trade-offs:
- Advantages: Horizontal scalability is straightforward. Any server node behind a round-robin load balancer can handle any request without session clustering or sticky sessions. Server crash recovery is instantaneous.
- Disadvantages: Network bandwidth overhead increases because repetitive metadata (e.g., large JWT bearer tokens and headers) must be transmitted in every individual request.
* REST: Resource-centric over HTTP. Best for public-facing developer APIs, mobile backends, and standard web integrations leveraging HTTP caching.
* GraphQL: Single endpoint query language. Clients request the exact fields needed, preventing over-fetching/under-fetching. Best for complex, interconnected UIs aggregating data across multiple microservices.
* gRPC: High-performance RPC using HTTP/2 framing and binary Protocol Buffers (Protobuf). Best for low-latency, internal polyglot microservice-to-microservice communication and bi-directional streaming.
* SOAP: Rigid XML protocol with strict WS-* security and transaction standards. Best for enterprise legacy banking, healthcare, and government contracts requiring formal WSDL contracts.
The Uniform Interface decouples the client from server implementation details so that both can evolve independently without breaking contracts.
It enforces 4 fundamental requirements:
- Identification of Resources: Individual resources are identified by stable, distinct URIs.
- Manipulation through Representations: Clients hold an abstract representation (e.g., JSON or XML) of a resource, not the database row directly.
- Self-Descriptive Messages: Each message contains enough metadata (e.g.,
Content-Type: application/json) for the receiver to know how to parse and handle it. - HATEOAS: The client navigates application states purely through links dynamically provided by the server representations.
The Layered System constraint dictates that a component cannot see beyond the immediate layer with which it is interacting.
Practical Implementations:
- Reverse Proxies & Load Balancers (e.g., NGINX, HAProxy): Distribute traffic without the client knowing the backend server IP.
- Edge Caches & CDNs (e.g., Cloudflare, Akamai): Serve cached representations from edge nodes without hitting origin servers.
- API Gateways: Handle authentication, TLS termination, and rate-limiting transparently.
Code on Demand allows a server to send executable code (e.g., JavaScript scripts, WebAssembly applets) to the client to extend its capabilities at runtime.
It is designated as optional because:
- It reduces visibility and introduces security vulnerabilities (arbitrary code execution risks on the client).
- Many non-browser API consumers (e.g., microservices, IoT devices, cron workers) lack execution runtimes to execute dynamic client-side scripts.
Common architectural violations include:
- Tunneling Actions inside GET/POST: Using verbs in URIs like
POST /deleteUser?id=5orGET /updateOrder. - Ignoring HTTP Status Codes: Returning
200 OKfor all responses while embedding errors in the body:{ "status": 500, "error": "Db Fail" }. - Stateful Server Sessions: Storing user shopping cart or step-by-step wizard context in server memory (session stickiness).
- Bypassing Content Negotiation: Hardcoding output formats without respecting
AcceptandContent-Typeheaders.
* Resource State: The actual data stored persistently on the server (e.g., database records representing an order, account balance, or user profile). It is uniform, shared across all users, and manipulated via HTTP methods.
* Application State: The current progression of a specific client within an interaction flow (e.g., which page of a multi-step checkout wizard the user is currently on).
In REST, Application State must be maintained entirely on the client (or embedded as hypermedia links), never stored in the server's session memory.
* HTTP/2: Introduces binary framing and multiplexing over a single persistent TCP connection. Eliminates the need for API bundling tricks (e.g., batch endpoints), supports header compression (HPACK), and removes Head-of-Line (HoL) blocking at the application layer.
* HTTP/3: Replaces TCP with UDP-based QUIC. Completely eliminates transport-level Head-of-Line blocking (packet loss on one stream does not stall adjacent streams) and enables 0-RTT handshakes and seamless network migration between Wi-Fi and mobile cellular data.
An API Contract is a machine-readable specification (OpenAPI Specification / OAS) that formally defines endpoints, supported methods, request schemas, authentication requirements, and status code responses.
Governance Benefits:
- Design-First Development: Enables front-end and back-end teams to agree on schemas and generate mocks before writing implementation code.
- SDK & Client Generation: Automatically produces strongly-typed client libraries (via OpenAPI Generator).
- Contract Testing: Validates that server runtime payloads strictly match published schemas, preventing breaking changes.
* RPC: Action-Centric. Focuses on executing remote procedures/verbs:
/cancelSubscription() or /transferFunds(). URIs describe operations. Often relies on custom payload envelopes.* REST: Entity/Resource-Centric. Focuses on nouns:
/subscriptions/{id} or /transfers. Actions are expressed uniformly through standardized HTTP verbs (POST, PUT, DELETE).
Yes. REST is an abstract architectural style, not a protocol standard:
- While HTTP is the ubiquitous choice, any protocol that provides uniform addressing (URIs), standardized verbs, content negotiation, and metadata headers can implement REST.
- Examples include CoAP (Constrained Application Protocol) used for RESTful IoT sensor devices over UDP, or custom internal message bus transports that adhere to Fielding's 6 constraints.
A Service Mesh (e.g., Istio, Linkerd) uses sidecar proxies (Envoy) to manage inter-service communication:
- External client traffic enters via an Ingress Gateway running REST over TLS.
- The mesh transparently intercepts internal calls, applying mutual TLS (mTLS), distributed tracing, rate-limiting, and retries.
- Internal services often translate external REST calls into lightweight gRPC/binary protocols behind the mesh boundary.
* Entity: The concrete business domain model or physical database row (e.g., a record in a SQL
Users table with internal hash keys).* Resource: A conceptual mapping exposed via a URI (e.g.,
https://api.example.com/users/42).* Representation: The serialized format (bytes) transmitted over the wire representing the current state of that resource (e.g., a JSON payload, an XML document, or a PDF stream).
HTTP Methods, Idempotency & Safety
* Safe Method: An HTTP method that does not alter server resource state (read-only). Safe methods can be pre-fetched, cached, and repeated without side effects.
Safe Methods:
GET, HEAD, OPTIONS, TRACE.* Idempotent Method: An HTTP method where executing the exact same request $N$ times consecutively produces the exact same server resource state as executing it once ($f(f(x)) = f(x)$).
Idempotent Methods:
GET, HEAD, PUT, DELETE, OPTIONS.Non-Idempotent Methods:
POST, PATCH.
*
PUT (Complete Replacement / Upsert):
- The client sends a complete representation of the resource. Any optional fields omitted in the request payload are overwritten with default values or set to
null. - Idempotent: Sending the identical complete payload 5 times results in the exact same resource state.
PATCH (Partial Mutation):
- The client sends only the delta (specific fields to be modified) without touching omitted properties.
- Non-Idempotent by default: A patch instructing
{ "increment": 5 }or appending items to an array changes the server state on every invocation.
Idempotency applies to server resource state, NOT the response status code.
When executing:
DELETE /orders/500 HTTP/1.1 -> 204 No Content (Order deleted)
DELETE /orders/500 HTTP/1.1 -> 404 Not Found (Order already absent)
After the first request, the resource is deleted. After the second and third requests, the resource remains deleted. The state of the server has not changed as a result of subsequent requests; therefore, DELETE satisfies the mathematical definition of idempotency.
Standardized specifications for processing
PATCH requests:
- JSON Merge Patch (
application/merge-patch+json): Sends a simple partial JSON object. Properties with values overwrite existing fields; properties assignednullare deleted. Limitation: cannot easily set a field to a literalnullvalue. - JSON Patch (
application/json-patch+json): Sends an explicit array of sequential operations (add,remove,replace,move,copy,test):
If the atomic[ { "op": "test", "path": "/status", "value": "pending" }, { "op": "replace", "path": "/status", "value": "shipped" }, { "op": "add", "path": "/trackingNumber", "value": "TRK98765" } ]testoperation fails, the entire patch aborts safely.
Standard
POST /payments requests are non-idempotent; network timeouts can lead to double billing if retried.
Idempotency Key Protocol (IETF Draft / Stripe Pattern):
- The client generates a unique UUID and passes it via header:
Idempotency-Key: 8d9e2a3b-4c5d-6e7f-8a9b-0c1d2e3f4a5b - The API Gateway / server verifies if the key exists in an in-memory cache (e.g., Redis):
- If new: Stores the key in a
PROCESSINGlock state, processes the payment, and saves the final response payload with a TTL (e.g., 24 hours). - If key exists and is completed: Bypasses business logic and returns the cached response directly.
- If key exists and is currently processing: Returns
409 Conflictor asks client to wait.
- If new: Stores the key in a
HEAD is identical to GET, except that the server must not return a response body (only headers are returned).
High-Value Use Cases:
- Resource Existence & Permission Check: Verifies if a resource exists (200 vs 404) or if the caller is authorized without downloading heavy payloads.
- Content Freshness Check: Reads
ETagorLast-Modifiedheaders to check if local caches are fresh before making a fullGETrequest. - Bandwidth Verification: Reads
Content-Lengthprior to downloading large multi-gigabyte files.
OPTIONS queries the target server to describe which communication options and HTTP methods are supported for a resource:
CORS Preflight: When a browser cross-origin request uses custom headers, non-simple content types (like
application/json), or methods like PUT/DELETE:
- The browser automatically sends a preflight
OPTIONSrequest before the real call:OPTIONS /api/orders/1 HTTP/1.1 Origin: https://app.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Authorization, Content-Type - The server responds with
204 No Contentcontaining allowed origins, headers, and verbs. If approved, the browser issues the actualPUTrequest.
According to RFC 9110 (HTTP Semantics):
- A client may send a payload on a
GETrequest, but the payload has no defined semantic meaning. - Servers, proxies, API gateways, and CDNs are allowed to reject, strip, or ignore bodies on
GETrequests. - Caches cannot use request bodies as part of cache keys.
- Best Practice: Never design APIs requiring a body in
GETrequests. If complex filtering requires an extensive JSON structure, usePOST /searchor custom media types.
*
POST /orders: Used when the server controls URI generation. The client posts the payload to a parent collection. The server creates the record, allocates an ID (e.g., ID 50), and returns 201 Created with Location: /orders/50. Multiple identical posts create multiple distinct orders.*
PUT /orders/50: Used when the client controls the URI / Primary Key. The client specifies the exact target address. If ID 50 does not exist, the server creates it; if it exists, it replaces it. Running it 5 times creates exactly one order at that address.
TRACE performs a loop-back diagnostic test. The server echoes the exact received request back to the client inside a message/http body, allowing clients to see changes made by intermediate proxies.
Security Vulnerability (Cross-Site Tracing / XST): An attacker utilizing a Cross-Site Scripting (XSS) exploit can issue a
TRACE request to the server. The server echoes the request back, exposing sensitive HttpOnly authentication cookies and authorization headers, completely defeating HttpOnly cookie defenses. Always disable TRACE at the web server/gateway.
Some legacy firewalls, corporate proxies, or HTML form clients support only
GET and POST, blocking verbs like PUT, DELETE, or PATCH.
Method Overriding: The client submits a
POST request and appends a tunneling header or query parameter:
POST /users/42 HTTP/1.1
X-HTTP-Method-Override: DELETE
The API gateway or middleware reads the header, overrides the request method internally, and routes it to the DELETE endpoint handler.
Modifying core domain resource state inside a GET request is a severe architectural violation:
- Web crawlers (Googlebot), browser prefetch engines, and CDNs will aggressively trigger state mutations automatically without user consent.
- Proxies and browsers cache
GETresponses, causing subsequent requests to be skipped entirely. - Benign Exceptions: Updating internal diagnostic counters, access logs, or last-login analytics timestamps is permitted as long as it does not mutate the resource's logical domain state.
* Safe / Idempotent Calls (
GET, PUT, DELETE): When an HTTP client (or load balancer) encounters a TCP timeout or 503 Service Unavailable, it can automatically retry the request safely without risking data corruption or duplicate transactions.* Non-Idempotent Calls (
POST, PATCH): Clients and reverse proxies must NEVER retry automatically on connection drops without an explicit idempotency protocol. The server may have completed the operation while only the response ACK packet was lost.
CONNECT converts an HTTP connection into a transparent two-way TCP tunnel:
CONNECT api.internal.corp:443 HTTP/1.1
Host: api.internal.corp:443
Primarily used by forward proxies and enterprise edge gateways to facilitate end-to-end encrypted TLS communication (HTTPS) between a client and a destination server without the proxy decrypting the payload.
*
PUT Race Conditions (Lost Updates): If Session 1 reads an order and updates status, while Session 2 reads the order and updates shippingAddress, the session that commits last with a full PUT overwrites and erases the other session's fields.*
PATCH Field Independence: Because PATCH sends discrete fields, modifying status and shippingAddress concurrently can be applied independently to database columns without conflict, significantly improving concurrency safety.
HTTP Status Codes & Standard Headers
HTTP status codes are 3-digit integers categorized by their first digit:
- 1xx (Informational): Request received; protocol handshake continuing (e.g.,
100 Continue,101 Switching Protocols). - 2xx (Successful): The request was successfully received, understood, and accepted (e.g.,
200 OK,201 Created,204 No Content). - 3xx (Redirection): Further action must be taken by the client to fulfill the request (e.g.,
301 Moved Permanently,304 Not Modified). - 4xx (Client Error): The request contains bad syntax, invalid payload, or unauthorized credentials (e.g.,
400 Bad Request,401 Unauthorized,404 Not Found). - 5xx (Server Error): The server failed to fulfill an apparently valid request due to internal crashes or dependency failures (e.g.,
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable).
*
401 Unauthorized (Authentication Failure): The client lacks valid authentication credentials. The user is unauthenticated (anonymous). The response must include a WWW-Authenticate challenge header indicating the expected auth scheme (e.g., Bearer realm="api").*
403 Forbidden (Authorization Failure): The client is successfully authenticated (the server knows who you are), but the user lacks the required roles, permissions, or scopes to access the resource. Re-authenticating with the same credentials will not alter the outcome.
*
200 OK: Standard success. Payload contains resource representations.*
201 Created: The request succeeded and a new resource was created. Must include a Location header containing the URI of the newly created resource.*
202 Accepted: Asynchronous batch processing. The request was accepted, but processing has not completed (and may fail later). Response typically includes a link to a status-polling endpoint.*
204 No Content: The action succeeded, but the response body is completely empty (used for DELETE or PUT updates where returning the entity is unnecessary).
*
400 Bad Request (Syntax & Structural Errors): The server cannot parse the request payload due to malformed syntax (e.g., broken JSON brackets, invalid query parameters, or mismatched data types like sending a string into an integer field).*
422 Unprocessable Entity (Semantic Validation Errors): The payload syntax is completely valid and parseable, but it contains business logic or domain constraint violations (e.g., startDate is after endDate, an email is already registered, or password complexity rules are failed).
In an architectural proxy or API gateway topology:
502 Bad Gateway: The gateway received an invalid or corrupted response from an upstream server (e.g., the upstream backend crashed, unexpectedly closed the TCP socket, or returned an invalid HTTP frame).504 Gateway Timeout: The gateway did not receive a response in time from the upstream backend server before its internal read timeout expired.
503 Service Unavailable indicates that the server is temporarily unable to process the request due to planned maintenance, thread-pool exhaustion, or transient CPU saturation.
The
Retry-After Header:
The response should include a Retry-After header informing the client how long to wait before attempting execution again:
HTTP/1.1 503 Service Unavailable
Retry-After: 30
Can be specified as a number of seconds (30) or as a standardized HTTP-date (Wed, 21 Oct 2026 07:28:00 GMT).
409 Conflict indicates that the request cannot be completed due to a conflict with the current state of the target resource:
- Optimistic Concurrency Failures: Mismatched version numbers or
ETagvalues. - State Machine Invariants: Attempting to transition an order from
CANCELLEDtoSHIPPED. - Duplicate Unique Constraints: Attempting to create a user account with an existing active primary key.
*
301 (Permanent) & 302 (Found / Temporary): Historically, user agents (browsers) erroneously changed non-GET methods (e.g., POST) into GET when following these redirects.*
307 Temporary Redirect: Guarantees that the redirected request must use the identical HTTP method and body as the original request.*
308 Permanent Redirect: The permanent equivalent of 307; guarantees that the method and body remain identical across redirection.
*
415 Unsupported Media Type (Request Body Rejection): The server rejects the format of the payload sent by the client because the server does not support the MIME type declared in the request's Content-Type header (e.g., client sent XML, but server only parses JSON).*
406 Not Acceptable (Response Negotiation Failure): The server cannot produce a response representation matching the requirements specified in the client's Accept header (e.g., client requests Accept: application/msgpack, but server can only emit JSON).
429 Too Many Requests indicates that the client has exceeded rate limits or throttling quotas within a designated time window.
Standard Accompanying Headers (IETF Draft):
HTTP/1.1 429 Too Many Requests
Retry-After: 60
RateLimit-Limit: 100
RateLimit-Remaining: 0
RateLimit-Reset: 60
412 Precondition Failed occurs when evaluating conditional headers (such as If-Match or If-Unmodified-Since):
PUT /documents/10 HTTP/1.1
If-Match: "v1.0"
If another process updated the document and the current server ETag is now "v2.0", the precondition fails. The server returns 412 and aborts the update, preventing the Lost Update Problem under optimistic concurrency.
The
Location response header indicates the URL to redirect a page to, or the location of a newly created resource.
Mandatory Situations:
- With
201 Created: Must contain the canonical URI of the newly created resource:HTTP/1.1 201 Created Location: /api/v1/users/987 - With
3xxRedirection responses: Directs the client to the new destination URI. - With
202 Accepted: Often points to a status monitoring URL.
The
Vary header informs caching intermediaries (proxies, CDNs, browsers) which request headers determine whether a cached representation can be served:
Vary: Accept, Accept-Encoding
If a cache holds a cached JSON representation, and an incoming request specifies Accept: application/xml, the cache inspects the Vary: Accept directive, realizes the representation must differ, and forwards the call to the origin server rather than serving the cached JSON.
Used when a client intends to send a large request payload (e.g., uploading a 1 GB video file):
- The client sends only the headers with an expectation header:
POST /uploads HTTP/1.1 Expect: 100-continue Content-Length: 1073741824 - The server evaluates authentication, permissions, and quota limits.
- If approved, the server returns
100 Continue, and the client streams the large body. If rejected, the server returns401or413 Payload Too Largeimmediately, saving massive network bandwidth.
Historically, non-standard and custom headers were prefixed with
X- (e.g., X-Correlation-Id, X-Requested-With).
RFC 6648 (Deprecated X- Prefix): The IETF formally deprecated the
X- prefix convention because when custom headers become standard protocols, migrating away from the X- name causes breaking changes.
Modern Standard: Name custom headers using descriptive, natural kebab-case names without
X- (e.g., Correlation-Id, Trace-Parent, Idempotency-Key).
URI Design, Resource Modeling & RPC vs REST
A well-architected RESTful URI space models domain nouns rather than procedural actions:
- Use Nouns, Never Verbs: URIs identify entities (e.g.,
/orders,/customers). The action to perform is expressed exclusively via standard HTTP verbs (GET,POST,PUT,DELETE). - Plural Nouns for Collections: Standardize on plural nouns (e.g.,
/productsrather than/product) so that reading the collection (GET /products) and reading an individual item (GET /products/123) share a consistent namespace. - Kebab-Case & Lowercase: URIs should be strictly lowercase using hyphens (kebab-case) for readability (e.g.,
/user-profiles). Avoid underscores and camelCase. - No Trailing Slashes or File Extensions: Omit trailing slashes (e.g., use
/items, not/items/) and avoid extensions like.jsonor.xml; format selection belongs in theAcceptheader.
Sub-resources represent parent-child relationships (e.g., an order containing line items):
GET /customers/42/orders
POST /customers/42/orders
The Deep Nesting Anti-Pattern:
Nesting beyond 2 levels (e.g., /regions/us/customers/42/orders/99/items/3) becomes brittle, hard to route, and leaks relational database schemas.
Best Practice (Flattening): Nest child resources only for creation or scoped collection listing. Once a child has its own unique primary identifier across the system, address it directly via a top-level flat resource:
GET /order-items/3
PUT /order-items/3
Pure CRUD maps neatly to database tables, but real-world domains feature complex business processes.
3 Architectural Approaches:
- Reify the Action into a First-Class Resource: Model the action as an entity that can be created:
POST /accounts/10/transfers Content-Type: application/json { "targetAccountId": 20, "amount": 500 } - Model State Transitions via Sub-resources:
POST /orders/123/cancellation POST /orders/123/approval - Partial State Mutation via PATCH:
PATCH /orders/123 Content-Type: application/json { "status": "cancelled" }
* Path Parameters (
/departments/{deptId}/employees/{empId}): Used to identify a specific resource or define a strict hierarchical scope. Missing path parameters result in a completely different route or 404 Not Found.* Query Parameters (
?status=active&sort=name): Used to filter, sort, paginate, or project the resource representation. They alter the attributes of the search, not the resource identity itself.* Headers (
Authorization, Accept, If-Match): Used to convey protocol-level and transport metadata (authentication, caching hints, content types) without polluting the business data space.
* Collection Resource: A group of entities where items are identified by unique keys:
/users (returns an array) and /users/42 (returns one user).* Singleton Resource: An entity that exists only once within the system or contextually once per authenticated caller:
GET /me
GET /profile
GET /configuration
Singleton URIs use singular nouns and omit IDs because the identity is inferred implicitly from the caller's authorization context.
RPC (Remote Procedure Call) is preferable over REST when:
- Operation-Centric Domains: The domain is composed of calculations, transformations, or ephemeral jobs rather than persistent entities (e.g.,
/calculate-tax,/translate-text,/compress-image). - Complex Atomic Multi-Entity Orchestration: Workflows that cross dozens of domain aggregates in a single synchronous step where forcing resource mappings feels unnatural.
- Internal Low-Latency Mesh: High-throughput microservice-to-microservice traffic using gRPC or JSON-RPC.
Matrix URIs (defined by Tim Berners-Lee) use semicolons (
;) rather than query strings (?) to apply parameters directly to individual segments of a hierarchical path:
GET /departments;location=ny/employees;status=active
Allows scoping parameters to a specific parent segment in the path hierarchy rather than applying them globally to the entire URI. Largely replaced in modern Web APIs by query parameters due to caching and router compatibility issues.
Standard REST maps one URI to one resource. For high-throughput updates on multiple items:
Design Options:
- Bulk Resource Endpoint: Use a dedicated sub-resource:
POST /users/bulk-import Content-Type: application/json { "items": [ { "name": "Alice" }, { "name": "Bob" } ] } - Batch Execution Endpoint: Accept an array of discrete HTTP-like operations and return a multi-status response:
The response typically returns{ "requests": [ { "method": "DELETE", "url": "/orders/1" }, { "method": "PATCH", "url": "/orders/2", "body": { "status": "shipped" } } ] }207 Multi-Status(RFC 4918) detailing success or failure per item.
Exposing raw sequential database keys (e.g.,
/invoices/1001) introduces severe vulnerabilities:
- ID Enumeration / BOLA (Broken Object Level Authorization): Attackers increment the integer in automated scripts to scrape records across all tenants.
- Business Intelligence Leakage: Competitors can register, observe sequential IDs, and deduce precise sales volumes or customer growth rates.
URLs are limited by browsers and proxies (often to ~2,048 characters). If complex multi-clause boolean search filters exceed this limit:
Recommended Patterns:
POST /searchorPOST /products/searches: Treat the search query itself as a resource. The client posts the complex JSON query payload, and the server returns matching results.- Persistent Search Resource: The client executes
POST /searchesto persist the query parameters. The server returns201 CreatedwithLocation: /searches/abc-123, allowing the client to executeGET /searches/abc-123/resultswith full HTTP caching support.
* Relative URI (
/orders/42): Requires the client to track and prepend the correct scheme, domain host, port, and gateway base path.* Absolute Canonical URI (
https://api.example.com/v1/orders/42): Self-contained and unambiguous.
Best Practice: Hypermedia links and
Location response headers should always emit Absolute Canonical URIs, allowing clients to follow links directly without resolving base path offsets across differing regional gateway hosts.
* Direct Multipart Upload:
POST /documents/10/files
Content-Type: multipart/form-data
* Two-Step Cloud Storage Pattern (Production Scalable):
- Client calls
POST /documents/10/attachmentswith file metadata (filename, size, MIME type). - Server validates permissions and generates a temporary Pre-Signed S3 / GCS Upload URL, returning
200 OK. - Client uploads the binary payload directly to cloud storage (S3), bypassing the API gateway and saving backend CPU/memory.
- Client notifies server of completion via
POST /documents/10/attachments/{id}/complete.
Resource Aliasing maps a semantic shortcut URI to a canonical identifier:
Example: Instead of forcing the client to first resolve its own user ID via
/users?email=... to construct /users/987654/settings:
GET /me/settings
GET /users/current/settings
The server resolves the current user identity internally from the bearer token and routes the request to the canonical resource representation transparently.
All non-ASCII and reserved URI characters must be Percent-Encoded (RFC 3986):
- Spaces in query strings should be encoded as
%20(or+in form-urlencoded contexts). - Reserved delimiters (
/,?,#,&,=) used as data inside values must be percent-encoded (e.g.,/tags/c%2B%2Bforc++). - API Gateways should reject unencoded control characters with
400 Bad Requestto prevent HTTP Request Smuggling.
Standard convention matrix:
| Component | Convention | Example |
|---|---|---|
| Scheme & Domain | Strictly lowercase | https://api.example.com |
| Path Segments | kebab-case, lowercase | /order-management/line-items |
| Query Keys | camelCase or snake_case | ?startDate=... or ?start_date=... |
| Headers | Hyphenated Kebab-Case | X-Correlation-Id, Idempotency-Key |
API Versioning Strategies, Deprecation & Breaking Changes
* 1. URI Path Versioning:
https://api.example.com/v1/users* 2. Query Parameter Versioning:
https://api.example.com/users?v=1 (or ?api-version=1.0)* 3. Custom Request Header Versioning:
X-API-Version: 1 (or API-Version: 2026-09-01)* 4. Content Negotiation / Media Type Versioning:
Accept: application/vnd.company.v1+json
* URI Path Versioning (
/v1/users):
- Pros: Highly visible, easy to test directly in browsers, clean routing at the API Gateway layer, straightforward CDN cache partitioning.
- Cons: Violates the pure REST principle that a resource identity (URI) should remain immutable over its lifecycle.
Accept: application/vnd.app.v1+json):
- Pros: Purest REST compliance. The URI represents the stable resource; only its representation format changes.
- Cons: Harder to test in browsers, complex gateway routing rules, and requires meticulous configuration of the
Vary: Acceptcaching header.
Instead of monolithic integer versions (
v1, v2), Date-Based Versioning uses release dates representing evolutionary changes:
Stripe-Version: 2026-08-15
How it works under the hood:
- The internal backend executes against a single canonical current schema.
- When an older client passes an older date version header, the API Gateway or framework runs an automated chain of Version Transformations (Gates) that backward-transforms the response payload down to that specific date's shape.
- Accounts lock into a default version at creation, preventing existing integrations from breaking when the platform updates.
* Non-Breaking (Backward-Compatible):
- Adding a new optional field to a request body.
- Adding a new field to a response body (assuming clients follow the Tolerant Reader pattern).
- Adding a new endpoint or new optional query parameter.
- Removing, renaming, or re-typing an existing field.
- Changing an existing optional request field into a mandatory required field.
- Modifying the data format (e.g., changing a date string format or moving from integer cents to float dollars).
- Changing HTTP status code semantics (e.g., switching from
200to202).
* Postel's Law (Robustness Principle): "Be conservative in what you do, be liberal in what you accept from others."
* Tolerant Reader Pattern: Client applications should consume only the exact fields they explicitly need and safely ignore unrecognized properties in response payloads rather than crashing when unknown properties appear.
This allows API providers to append new fields to existing payloads without breaking older client integrations.
Use standardized IETF RFC 8594 headers:
HTTP/1.1 200 OK
Deprecation: @1788739200
Sunset: Wed, 11 Nov 2026 00:00:00 GMT
Link: <https://api.example.com/migration-v2>; rel="deprecation"; type="text/html"
Deprecation: Informs the client that the endpoint or version is deprecated (can be booleantrueor a Unix timestamp).Sunset: Specifies the exact date and time when the API version will be permanently turned off and return 410 Gone.Link: Points developers to the official migration documentation.
SemVer follows
MAJOR.MINOR.PATCH:
- MAJOR: Incompatible breaking changes (requires bumping URI/header version:
v1→v2). - MINOR: Backward-compatible new functionality (new endpoints, new response fields). No version bump in URI.
- PATCH: Backward-compatible bug fixes and internal performance enhancements.
/v1, not /v1.2.4). Patch and minor updates are deployed continuously without consumer disruption.
Follow the Expand and Contract Pattern (Parallel Change):
- Expand: Add the new database column alongside the old column. Update the database layer to dual-write to both old and new columns.
- Migrate: Backfill existing historical records from the old column to the new column.
- Expose: Deploy API
v2reading from the new column, while APIv1continues reading from the old column. - Contract: Once API
v1is decommissioned (post-sunset), drop the old column from the physical database schema.
*
404 Not Found: The server cannot find the requested resource. The absence may be temporary, accidental, or due to a typo. Search bots and clients may retry.*
410 Gone: The target resource or API version was deliberately removed and is permanently unavailable with no forwarding address. Clients, proxies, and web crawlers should remove references immediately and cease issuing future requests.
API Gateways (e.g., Kong, Apisix, AWS API Gateway) decouple version routing from physical microservice deployments:
- Path Transformation: Routes
/v1/ordersto Service A (v1 Docker container) and/v2/ordersto Service B (v2 container). - Header Inspection: Inspects
Acceptor custom version headers and forwards traffic to the corresponding upstream service cluster. - Telemetry Tracking: Tracks request metrics by version, pinpointing remaining active consumers on deprecated versions before sunset cutoffs.
* URI-based (
/v1/items vs /v2/items): Out-of-the-box safe. Because the URI string is the default cache key across all proxies, CDNs, and browsers, responses for v1 and v2 are segregated automatically.* Header-based (
Accept: ...v1 vs Accept: ...v2): Prone to cache pollution. If the server fails to include a Vary: Accept header, an intermediate proxy cache will store the v1 response and erroneously serve it to a subsequent client requesting v2, causing silent runtime failures.
Pioneered by platforms like GraphQL and Salesforce:
Instead of cutting monolithic version numbers, the API evolves continuously:
- New fields are appended directly to types.
- Old fields are marked as
@deprecatedwith clear reasons. - Fields are never removed until telemetry proves 0% consumption over an extended grace period.
- Eliminates massive multi-year migration projects and client churn associated with hard major version cuts.
SDKs and APIs maintain distinct lifecycles:
- An SDK package version (e.g.,
npm install stripe@12.4.0) uses standard SemVer for library dependencies, bug fixes, and runtime engine updates. - The SDK hardcodes and pins a specific underlying API version header (e.g.,
Stripe-Version: 2026-08-15) in every outbound HTTP request. - Updating the SDK to a new major version updates the pinned API version header predictably.
Supporting more than 2 or 3 active versions simultaneously introduces severe engineering friction:
- Codebase Bloat: Hundreds of conditional branches (
if (version === 'v1')) or separate microservice deployments. - Testing Combinatorics: Every bug fix and security patch must be verified across all active version matrices.
- Database Inefficiencies: Maintaining backward-compatible views, trigger synchronization, and dual-write routines degrades performance.
Brownout Testing (Screaming Test): Before permanently deleting an old API version on its sunset date, intentionally introduce temporary, planned outages on that version:
- Schedule a 1-hour window where the deprecated version returns
410 Goneor503 Service Unavailablewith migration headers. - Repeat with increasing durations (e.g., 4 hours, then 24 hours).
- This forces sluggish client engineering teams to notice failing health checks and migrate before the hard cutoff.
Filtering, Sorting & Pagination Strategies
* Offset-Based (
?page=50&limit=20 or ?offset=1000&limit=20):
- Maps to SQL
OFFSET 1000 LIMIT 20. - Pros: Allows jumping directly to arbitrary page numbers (e.g., "Go to page 10").
- Cons: Degrades to $O(N)$ on deep pages (the DB must read and discard 10,000 rows). Subject to Page Drift (skipped or duplicate rows when inserts/deletes occur mid-browsing).
?after=eyJpZCI6MTAxfQ==&limit=20):
- Maps to SQL
WHERE id > 100 ORDER BY id LIMIT 20. - Pros: Constant $O(1)$ indexed performance regardless of page depth. Immune to page drift.
- Cons: Cannot jump to arbitrary pages; only supports forward (and backward) sequential traversal.
Page Drift occurs when data modifications occur between page requests:
Scenario:
- Client reads Page 1 (items 1–10). Item 10 is Order #500.
- A new Order #1 is inserted at the top of the table. All records shift down by 1 offset.
- Client requests Page 2 (
OFFSET 10 LIMIT 10). - Because of the shift, Order #500 is now at offset index 11.
- Page 2 returns Order #500 again. The client displays duplicate records. (Conversely, deletions cause items to be skipped entirely).
Never expose raw database columns directly in cursor keys; serialize and encode them into an opaque token:
// Server generates cursor for last item (created_at: 1788739200, id: 105):
const rawCursor = JSON.stringify({ t: 1788739200, id: 105 });
const cursor = Buffer.from(rawCursor).toString("base64url");
// Emitted as: ?after=eyJ0IjoxNzg4NzM5MjAwLCJpZCI6MTA1fQ
Why Opaque Tokens are Mandatory:
- Prevents clients from guessing or forging cursors.
- Allows the backend to change internal sorting criteria (e.g., switching from single ID to composite keys) without breaking client integration contracts.
Standardize on a clean, comma-separated format prefixing sort directions:
GET /products?sort=-price,name
-prefix denotes descending order (-price→ORDER BY price DESC).- Absence of prefix denotes ascending order (
name→name ASC). - Alternative readable syntax:
?sort=price:desc,name:asc. - Security Rule: Whitelist allowed sort keys in the backend to prevent SQL injection or un-indexed sorting performance degradation.
Embed pagination metadata inside an envelope or via HTTP
Link headers:
{
"data": [ { "id": 101 }, { "id": 102 } ],
"pagination": {
"limit": 20,
"hasMore": true,
"nextCursor": "eyJpZCI6MTAyfQ",
"totalRecords": 1540
},
"links": {
"self": "https://api.example.com/items?after=...&limit=20",
"next": "https://api.example.com/items?after=eyJpZCI6MTAyfQ&limit=20"
}
}
Instead of polluting JSON payloads with metadata wrappers, RFC 5988 transmits pagination links in standard HTTP headers:
HTTP/1.1 200 OK
Link: <https://api.example.com/users?page=3&limit=20>; rel="next",
<https://api.example.com/users?page=1&limit=20>; rel="prev",
<https://api.example.com/users?page=50&limit=20>; rel="last"
Allows the response body to remain a pure, unpolluted array of entities: [ {...}, {...} ].
Two widely accepted industry patterns:
* 1. Bracket Syntax / Sub-Keys (The Stripe / Google Pattern):
GET /orders?created[gte]=2026-01-01&created[lt]=2026-02-01&status[in]=paid,shipped
* 2. Colon Operator Prefix (The GitHub Pattern):
GET /products?price=gt:100&category=eq:electronics
Maps directly to query parser abstractions without ambiguous string parsing.
Sparse Fieldsets (JSON:API pattern): Allows the client to request strictly the specific properties needed for its view:
GET /users/42?fields=id,name,email
Performance Benefits:
- Reduces payload byte size across mobile networks.
- Allows the backend to execute targeted SQL queries (
SELECT id, name, email FROM users) rather thanSELECT *, leveraging Covering Indexes and reducing database I/O.
To solve the N+1 query problem on mobile clients:
GET /orders/105?include=customer,items.product
Instructs the server to eagerly load and embed related child models directly into the primary response payload, eliminating the need for the client to execute separate sequential round-trips for each related entity.
Returning
totalRecords: 50000000 requires the database to execute:
SELECT COUNT(*) FROM orders WHERE status = 'pending';
The Bottleneck:
On multi-million row tables, COUNT(*) forces the database to scan entire non-clustered index trees or table pages. While fetching the 20 records takes 2 milliseconds, calculating the total count takes 2,500 milliseconds.
Best Practice: Omit total counts on high-volume feeds; use a simple boolean
hasMore: true (fetched by querying LIMIT 21 for a 20-item page).
Keyset Pagination converts cursor bounds directly into an indexed B-Tree seek:
-- For multi-column sort: ORDER BY created_at DESC, id DESC
SELECT id, created_at, amount
FROM orders
WHERE (created_at < @cursor_created_at)
OR (created_at = @cursor_created_at AND id < @cursor_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
Utilizes composite indexes on (created_at, id) to seek directly to the exact boundary row in $O(\log N)$ time, bypassing millions of preceding rows with zero performance drop.
If an API accepts an unconstrained limit parameter:
GET /orders?limit=10000000
A malicious or poorly written client can trigger server out-of-memory crashes and database lockups.
Defensive Rules:
- Enforce a default limit (e.g.,
limit = 20) if omitted. - Enforce a strict maximum cap (e.g.,
max_limit = 100). If a client requestslimit=5000, clamp it silently to 100 or reject with400 Bad Request.
Standardize on a dedicated
q parameter for free-text searches, combined with structured filter keys:
GET /articles?q=kubernetes+mesh&category=devops&status=published
The backend routes q to full-text search engines (Elasticsearch, OpenSearch, or Postgres tsvector), while structured attributes (category, status) apply as exact boolean filters.
Cursor pagination relies entirely on B-Tree index ordering:
- The sort column(s) must be backed by an index matching the sort direction.
- A deterministic tie-breaker column (usually the primary key
id) must be appended to the index to ensure deterministic ordering when non-unique columns are sorted:CREATE INDEX IX_Orders_Created_Id ON orders (created_at DESC, id DESC); - Without this index, cursor queries fall back to full table scans, destroying performance.
* Server-Side Filtering: Executed by the database before data is serialized. The client receives only matching rows. Mandatory for large datasets (> 1,000 items) to conserve bandwidth, CPU, and database memory.
* Client-Side Filtering: The client downloads the complete dataset once and filters in-memory (e.g., in browser state). Acceptable only for small, static lookup tables (e.g., country lists, currency codes) where instant instantaneous UI search without network latency is desired.
Content Negotiation, Media Types & Binary Payloads
Content Negotiation (ConNeg) is the mechanism that enables a client and server to agree on the best representation format for a given resource when multiple representations are available.
Server-Driven Negotiation Workflow:
- The client sends preference headers with the request:
Accept: Desired media type (e.g.,application/json,application/xml).Accept-Charset: Character encoding (e.g.,utf-8).Accept-Encoding: Compression algorithms (e.g.,gzip,br).Accept-Language: Natural language (e.g.,en-US,fr).
- The server evaluates its available representations against the client's preferences.
- The server emits the best matching representation and marks the selection in the
Content-Typeheader. If no acceptable representation can be produced, it returns406 Not Acceptable.
Quality values (
q) are floating-point numbers between 0.0 and 1.0 (defaulting to 1.0 if omitted) that declare client preference weights:
Accept: application/json;q=1.0, application/xml;q=0.8, text/plain;q=0.2, */*;q=0.1
Evaluation Hierarchy:
- Highest
qfactor wins: The server attempts to satisfyapplication/jsonfirst. - Specificity Rule: If two types have identical
qfactors, more specific MIME types take precedence over wildcards:text/html;level=1>text/html>text/*>*/*.
Vendor-specific media types use the
application/vnd.<vendor>.<resource>+<format> syntax defined by RFC 6838:
Accept: application/vnd.mycompany.order.v2+json
Content-Type: application/vnd.mycompany.order.v2+json
Architectural Benefits:
- Schema Typing & Contract Binding: Distinguishes arbitrary JSON dictionaries from a strictly defined domain contract without polluting URLs with version prefixes.
- Evolutionary Versioning: Clients request exactly the schema variation they understand, enabling seamless backward-compatible transformations at the gateway layer.
*
application/json: Generic JSON container. It carries no inherent semantic rules regarding how fields, entities, or errors are structured.*
application/problem+json (RFC 9457): A standardized media type specifically designed for reporting machine-readable API error details. It guarantees that consumers can parse predefined fields (type, title, status, detail, instance) deterministically, regardless of which microservice produced the failure.
* Request Body Failure (
415 Unsupported Media Type): When the client uploads an unsupported payload format declared via Content-Type (e.g., client sends Content-Type: text/xml, but the server only accepts application/json). The server should return an Accept-Post or Accept-Patch header declaring supported incoming formats.* Response Negotiation Failure (
406 Not Acceptable): When the client requests an unproducible format via Accept (e.g., Accept: application/msgpack on a server that only produces JSON).
In Agent-Driven Negotiation:
- The client requests a resource.
- The server responds with
300 Multiple Choicescontaining an index/list of available representations with their distinct URLs and metadata. - The client evaluates the list and issues a second request to the specific representation URL chosen.
*
Content-Type: Declares the underlying format and data schema of the representation (e.g., application/json; charset=utf-8). It tells the consumer how to deserialize the payload.*
Content-Encoding: Declares any lossless transformation or compression wrapper applied to the payload over the wire (e.g., gzip, br, deflate). It tells the recipient what decompression codec must be applied before deserializing according to the Content-Type.
Adopt binary serialization (Protobuf, MessagePack, FlatBuffers) when:
- High-Frequency / High-Volume Data: Thousands of requests per second where CPU serialization/deserialization time and GC allocation in JSON text parsing become the dominant server bottleneck.
- Bandwidth Constraints: Low-bandwidth mobile networks, satellite links, or IoT telemetry where binary compaction saves 40%–70% payload size over wire.
- Internal Microservice Meshes: East-west internal communication where human readability is unnecessary and strict typing contracts (via
.protofiles) are desired.
Content-Disposition instructs client user-agents how to display or download the returned payload:
- Inline (
inline): Renders the content directly within the browser window if supported (e.g., displaying a PDF or image inline):Content-Disposition: inline - Attachment (
attachment): Forces the browser to trigger a "Save As" download dialog with a suggested filename:Content-Disposition: attachment; filename="invoice_2026.pdf"; filename*=UTF-8''invoice_2026.pdf
The
Range request header asks the server to return only a specific subset of bytes from a large resource:
GET /videos/sample.mp4 HTTP/1.1
Range: bytes=1048576-2097151
Server Response:
- If supported, returns
206 Partial Contentwith aContent-Rangeheader declaring returned byte offsets and total file size:HTTP/1.1 206 Partial Content Content-Range: bytes 1048576-2097151/52428800 Content-Length: 1048576 - Enables video/audio seeking and allows clients to seamlessly resume aborted downloads without restarting from byte 0.
* In HTTP/1.1:
Transfer-Encoding: chunked allows the server to stream dynamically generated data without knowing the total Content-Length in advance. Data is streamed in chunks, each prefixed with its hexadecimal byte length, terminating with a zero-length chunk (0\r\n\r\n).* In HTTP/2 and HTTP/3:
Transfer-Encoding: chunked is explicitly forbidden. HTTP/2 natively implements multiplexed DATA frames that carry their own length indicators, making chunked transfer encoding completely redundant.
Never trust the client-supplied
Content-Type header or file extension alone (attackers can upload an executable disguised as image/png).
Multi-Layered Validation Strategy:
- Magic Number / File Signature Validation: Inspect the first 16 to 32 raw bytes of the uploaded binary stream to verify actual file signatures (e.g., PNG files always start with
89 50 4E 47 0D 0A 1A 0A). - Extension Whitelisting: Reject any file containing secondary extensions (e.g.,
malware.php.png). - Storage Isolation: Store files outside the web server root or upload directly to isolated cloud object storage (AWS S3, Azure Blob) with
Content-Typeoverridden cleanly and execute permissions stripped.
Because
PATCH operations can accept various structural patch formats (e.g., JSON Patch vs JSON Merge Patch), a server emits the Accept-Patch response header to inform clients which patch media types are supported:
HTTP/1.1 200 OK
Accept-Patch: application/json-patch+json, application/merge-patch+json
Often discovered by issuing an OPTIONS request against the target resource before executing modifications.
Historically, HTTP headers used inconsistent, ad-hoc syntax (some used commas, others semicolons or quotes), making parsing complex and prone to security bugs.
RFC 8941 (Structured Fields) establishes standard, unambiguous data types for HTTP headers:
- Lists, Dictionaries, Parameters, Booleans (
?1,?0), Integers, and Byte Sequences. - Adopted by modern IETF drafts such as
RateLimit-*,Priority, andDeprecationheaders for strict parsing safety.
Configure unified route handlers with dynamic content formatters:
POST /reports/generate HTTP/1.1
Content-Type: application/json
Accept: application/pdf
The request body accepts an input parameter schema serialized in JSON, processes domain business logic, and checks the Accept header to stream back an analytical PDF binary document with Content-Type: application/pdf. Content negotiation cleanly isolates the incoming configuration format from the emitted artifact format.
HATEOAS, Hypermedia Controls & Standard Formats
HATEOAS (Hypermedia As The Engine Of Application State) is the constraint that clients interact with a network application entirely through hypermedia links provided dynamically in resource responses.
Why it represents Level 3 (Full REST Maturity):
- The client requires zero hardcoded knowledge of out-of-band workflow URLs beyond the initial root endpoint (
/api). - As resources transition state (e.g., an order moves from
PENDINGtoPAID), the server dynamically includes, alters, or removes actionable links (e.g., introducing acancellink while removing apaylink). - Enables backends to alter routing topology without breaking clients.
HAL (
application/hal+json) is a simple, standardized hypermedia convention separating links and embedded child models using two reserved properties:
{
"id": 105,
"total": 49.99,
"status": "pending",
"_links": {
"self": { "href": "/orders/105" },
"customer": { "href": "/customers/42" },
"cancel": { "href": "/orders/105/cancellation" }
},
"_embedded": {
"items": [
{ "productId": "A1", "qty": 1, "_links": { "self": { "href": "/items/A1" } } }
]
}
}
JSON:API is a highly structured, opinionated specification designed to standardize REST requests and responses:
{
"data": {
"type": "articles",
"id": "1",
"attributes": { "title": "RESTful Architecture" },
"relationships": {
"author": {
"links": { "related": "/articles/1/author" },
"data": { "type": "people", "id": "9" }
}
},
"links": { "self": "/articles/1" }
}
}
Standardizes error envelopes, sparse fieldsets (?fields=...), resource inclusion (?include=...), sorting, and cursor pagination out of the box.
While HAL focuses almost exclusively on read navigation links (
_links), Siren (application/vnd.siren+json) provides explicit support for modeling state mutations via an actions array:
{
"class": ["order"],
"properties": { "orderNumber": 42 },
"actions": [
{
"name": "cancel-order",
"title": "Cancel Order",
"method": "POST",
"href": "/orders/42/cancel",
"type": "application/json",
"fields": [
{ "name": "reason", "type": "text" }
]
}
]
}
Informs the client not only where to go, but exactly which HTTP method and form fields must be submitted.
The
rel attribute describes the semantic relationship between the current resource and the linked target:
- Standard IANA Link Relations (RFC 8288): Predefined standard relations such as
self,next,prev,first,last,collection,author, anditem. - Custom Extension Relations: When a domain relationship is proprietary, it should be formatted as a fully qualified URI to avoid semantic collisions:
"rel": "https://api.example.com/rels/refund-payment"
Despite theoretical elegance, pure HATEOAS adoption in industry is low due to pragmatic constraints:
- Client Complexity: Frontend developers prefer predictable, strongly typed client SDKs (TypeScript, Swift, Kotlin) mapped to concrete schemas over dynamic link-following parsers.
- Bandwidth Overhead: Injecting metadata link dictionaries into thousands of array items multiplies JSON payload sizes by 2x–5x.
- Mobile UI Realities: Modern UIs need to know which buttons to render ahead of time; discovering allowed actions dynamically can complicate responsive design state machines.
In workflow systems (e.g., banking loans, order fulfillment), resource state transitions are governed by business invariants:
* When an order is
CREATED, the payload includes links: {"rel": "pay"} and {"rel": "cancel"}.* Once paid (
PAID), the server response omits the pay link, retains cancel, and introduces {"rel": "ship"}.* Once shipped (
SHIPPED), the cancel link vanishes, replaced by {"rel": "track"}.The client UI inspects the link presence to toggle button visibility; business workflow logic remains centralized entirely on the server.
RFC 6570 defines syntax for expressing parameterized URIs within hypermedia structures:
{
"_links": {
"search": {
"href": "/orders{?status,page,limit}",
"templated": true
}
}
}
Informs the client client-side router how to interpolate parameters (e.g., query strings or path variables) into a valid URL without guessing the parameter names.
*
_links: Contains metadata pointers (URIs) to related resources. The client must make an additional network round-trip to fetch the data.*
_embedded: Contains the fully materialized payload of related resources embedded directly inside the parent response, eliminating the network latency of following the link while retaining hypermedia structure.
Collection+JSON (
application/vnd.collection+json) is designed specifically to represent management collections:
- Provides a root
collectioncontainer holdingitems,queries, and atemplate. - The
templateobject explicitly defines the schema and fields required for a client to submit aPOSTrequest to create a new item within the collection, making the API self-documenting.
By utilizing standard RFC 8288 Web Linking headers:
HTTP/1.1 200 OK
Link: </orders/105/cancel>; rel="cancel", </customers/42>; rel="customer"
Enables clean hypermedia controls while keeping the payload body a pure, unpolluted domain model (ideal for consumers that reject custom wrappers like _links).
* Cache Coherence: When resources embed full canonical URLs in their
_links array, caching proxies can index and invalidate individual sub-resources cleanly.* Cache Pollution Risk: If hypermedia links embed ephemeral, user-specific authorization tokens directly inside query parameters (e.g.,
/orders/42?token=xyz), the URL changes per user, completely defeating shared CDN caching. Links must remain clean and generic.
JSON-LD (JSON for Linking Data,
application/ld+json) enriches standard JSON with global machine-readable semantics:
{
"@context": "https://schema.org",
"@type": "Person",
"name": "Jane Doe",
"jobTitle": "Lead Architect"
}
By defining an @context, keys are unambiguously mapped to globally defined ontological schemas (e.g., Schema.org), allowing disparate enterprise systems and search engine scrapers to understand domain concepts automatically without manual mapping.
The root API entry point (
GET /api) should serve as an architectural directory:
{
"_links": {
"self": { "href": "/api" },
"orders": { "href": "/api/orders" },
"users": { "href": "/api/users" },
"docs": { "href": "https://docs.example.com", "type": "text/html" }
}
}
A client needs to configure only the root address; all sub-service namespaces and routing boundaries are discovered at runtime.
* CPU & Memory Allocation: Constructing link objects, evaluating permissions per row (to decide if the
cancel link is allowed for the active user), and formatting URIs for lists of 1,000 entities incurs high CPU serialization overhead and GC allocations.* Network Serialization Bloat: JSON payload string lengths typically expand by 30% to 100%, consuming more bandwidth and parsing time on low-power mobile devices.
Error Handling, Problem Details & Fault Tolerance
RFC 9457 (superseding RFC 7807) defines a standardized, machine-readable JSON structure for communicating API errors under the media type
application/problem+json:
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 403,
"detail": "Your account balance of $25.00 is lower than the requested $50.00.",
"instance": "/accounts/123/transactions/tx-987"
}
type(URI reference): A stable URI that uniquely identifies the specific error condition and points to human-readable documentation. Defaults to"about:blank"if omitted.title(string): A short, human-readable summary of the problem type. It must not change between occurrences of the same error type (e.g., always "Bad Request").status(number): The exact HTTP status code generated by the origin server for this occurrence (must match the protocol status code).detail(string): A specific human-readable explanation of this particular occurrence of the problem.instance(URI reference): A unique URI identifying the specific resource or transaction occurrence where the failure happened (used for log correlation).
RFC 9457 allows arbitrary extension members:
{
"type": "https://api.example.com/errors/validation-error",
"title": "Invalid Request Parameters",
"status": 422,
"detail": "One or more fields failed validation checks.",
"instance": "/users",
"errors": [
{ "field": "email", "message": "Must be a valid corporate email address." },
{ "field": "password", "message": "Password must be at least 12 characters long." }
]
}
Returning
200 OK with {"success": false, "error": "Database down"} breaks HTTP architecture:
- Breaks Caching: Intermediate edge proxies and CDNs assume
200 OKis healthy and cache the error response, serving database failure screens to all subsequent visitors. - Disables Circuit Breakers: API gateways and resilience libraries (e.g., Envoy, Polly) monitor
5xxerror ratios; returning200hides outages, preventing automated failovers. - Breaks Monitoring / APM: Monitoring systems (Datadog, Dynatrace) track HTTP status codes for SLA uptime alerting.
Returning raw unhandled exceptions (e.g., Java/Node stack traces, SQL error dumps):
- Information Disclosure (CWE-209): Exposes internal database table schemas, SQL dialect types, internal server paths, framework versions, and third-party library names.
- Attack Surface Mapping: Attackers analyze exposed library versions to execute known CVE exploits or construct targeted SQL injection attacks.
A Correlation ID is a unique identifier generated at the edge gateway that travels across all downstream microservices:
- The API gateway receives a request. If
X-Correlation-Id(or W3Ctraceparent) is missing, it generates a new UUID. - Every internal microservice passes this header forward in outbound HTTP or gRPC calls.
- All application log entries across all servers tag this ID.
- When an error occurs, the server returns the ID in the problem response:
{"traceId": "c8a1b2..."}, allowing engineers to trace the full distributed execution tree in logs instantly.
A centralized middleware component placed at the outer boundary of the web application pipeline:
- Catches any unhandled exception escaping controller or endpoint handlers.
- Clears partially written response buffers to avoid corrupted payloads.
- Inspects exception type: translates domain validation exceptions into
400/422, authorization exceptions into401/403, and unhandled system crashes into500. - Serializes and writes an RFC 9457
ProblemDetailsJSON structure with appropriateContent-Typeheaders.
When an API is under extreme load or performing database failovers:
- The server should shed load fast and return
503 Service Unavailablewith aRetry-Afterheader (e.g.,Retry-After: 30). - Clients must implement Exponential Backoff with Jitter: $$\text{Delay} = 2^{\text{attempt}} \times \text{BaseDelay} \pm \text{RandomJitter}$$
- Adding random jitter prevents the Thundering Herd Problem (Retry Storm) where thousands of retrying clients strike the recovering database at the exact same millisecond.
* Client-Recoverable Errors (4xx): Caused by the caller (e.g., missing parameter, expired token, insufficient balance). The response should provide actionable guidance explaining how the client can correct the request before retrying.
* Fatal System Faults (5xx): Caused by internal infrastructure failures (e.g., database connection loss, disk out-of-space, downstream timeout). The client cannot fix the request; it must retry later with backoff or alert an administrator.
In a microservices distributed Saga:
- If Step 3 (Payment Service) fails, the orchestrator triggers Compensating Transactions in reverse order (e.g., releasing inventory held in Step 2).
- Compensating endpoints (e.g.,
POST /inventory/release) must be strictly idempotent: if network latency causes the compensation call to be retried, the system must handle it without double-refunding or corrupting inventory levels.
Error Masking is a defensive gateway pattern where internal microservice failure details are sanitized before reaching external public clients:
- If an internal service crashes with a raw 500 error containing connection strings or internal microservice hostnames (
http://order-srv.internal.corp:8080), the gateway strips the body. - It generates a generic RFC 9457 error:
"An unexpected internal error occurred", logging the internal trace privately.
When processing a batch of 100 items where 90 succeed and 10 fail:
Do not fail the entire batch or return a single generic status code:
- Return
207 Multi-Status(RFC 4918) or200 OKwith an itemized result array:{ "summary": { "total": 100, "succeeded": 90, "failed": 10 }, "results": [ { "id": "item-1", "status": 201 }, { "id": "item-2", "status": 422, "error": { "title": "Duplicate SKU" } } ] } - Enables callers to commit successful items and retry only the specific failed items.
* HTTP Status Codes: Transport and protocol-level states defined by RFCs (only ~60 codes exist globally). They indicate high-level outcomes (e.g.,
403 Forbidden).* Business Domain Exceptions: Specific application logic violations (thousands can exist within an enterprise).
Mapping: Domain exceptions must be mapped into the closest appropriate HTTP status code category, with the specific business error code declared in the RFC 9457
type URI or extension properties (e.g., "code": "CARD_EXPIRED" under status 422).
Well-designed SDKs do not treat errors as plain unparsed strings:
- They inspect the response
Content-Type: ifapplication/problem+json, they deserialize the payload into a strongly-typedApiExceptionclass. - They expose properties for
status,type,traceId, andvalidationErrorsdirectly on the exception object. - They automatically catch
429and503responses withRetry-Afterheaders and manage backoff retries transparently under the hood.
* Machine Codes Over Strings: The API should return stable, machine-readable error codes (e.g.,
USER_NOT_FOUND) rather than localized sentences, allowing the client application to translate messages in the user's native language.* Respecting
Accept-Language: If the server provides localized detail strings, it must inspect the client's Accept-Language header (e.g., fr-FR) and include Vary: Accept-Language in the response headers.
OAuth 2.1, JWT Patterns & Token Architecture
OAuth 2.1 consolidates and deprecates vulnerable legacy practices accumulated in OAuth 2.0:
- PKCE Mandatory: PKCE (Proof Key for Code Exchange) is now strictly mandatory for all clients using the Authorization Code flow, including confidential server-side clients.
- Implicit Grant Deprecated: The Implicit Flow (which returned tokens directly in URL hash fragments) is completely omitted due to URL leakage and access token injection vulnerabilities.
- Resource Owner Password Credentials (ROPC) Deprecated: The password grant is omitted because it forces clients to handle and store raw user credentials, breaking zero-trust isolation.
- Exact Redirect URI Matching: Wildcard redirect URIs and partial matching are forbidden to prevent token interception.
- Bearer Token Restrictions: Bearer tokens in URL query strings are explicitly forbidden.
A JWT (RFC 7519) consists of three Base64URL-encoded segments separated by periods:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTYiLCJuYW1lIjoiSm9obiJ9.K9X...
- Header: Metadata declaring the token type (
"typ": "JWT"), signing algorithm (e.g.,"alg": "RS256"), and key identifier ("kid": "auth-key-1"). - Payload (Claims): Statements about an entity, including registered claims (
sub,iss,aud,exp,nbf,iat,jti) and custom domain scopes/roles. - Signature: Cryptographic hash calculated over
Base64URL(Header) + "." + Base64URL(Payload)using a shared secret (HMAC) or private key (RSA/ECDSA). Verifies that the payload was not tampered with in transit.
* Symmetric Signing (HS256): Uses a single shared secret key to both sign and verify tokens.
The Microservice Risk: Every downstream API that validates incoming tokens must possess the secret key. If one microservice is compromised, an attacker can use that shared secret to forge valid arbitrary tokens across the entire enterprise ecosystem.
* Asymmetric Signing (RS256 / ES256 - Recommended): Uses a public/private key pair.
The central Identity Provider (IdP) signs tokens exclusively using its private key. Downstream resource APIs validate tokens using the publicly available public key (via JWKS). Resource APIs can verify authenticity, but lack the cryptographic ability to forge tokens.
JWKS (RFC 7517) is a standardized JSON document hosted by the Authorization Server (typically at
/.well-known/jwks.json) publishing public cryptographic keys:
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "key-2026-q1",
"alg": "RS256",
"n": "u1...mQ",
"e": "AQAB"
}
]
}
Automated Key Rotation:
Resource APIs cache public keys from the JWKS endpoint. When a token arrives signed with a new kid (Key ID), the API detects the cache miss, refreshes the JWKS from the IdP, and verifies the token without service downtime or manual certificate deployments.
* The "None" Algorithm Exploit: An attacker modifies the JWT header to
{"alg": "none"}, sets claims to {"role": "admin"}, and strips the signature. Flawed verification libraries accepted the token without executing signature validation.* Algorithm Confusion (RS256 vs HS256): An attacker takes an API configured for RS256, changes the token header to
HS256, and signs the token using the server's public RSA key as the HMAC secret key. If the server verifies the token using its public key as the secret parameter, the signature validates as valid.
Defense: Always enforce strict, explicit algorithm whitelisting in verification middleware (e.g.,
algorithms: ['RS256']); never trust the alg header supplied by the client.
Because JWTs are self-contained, an API verifies tokens purely via signature and expiration without querying a database. This creates an architectural paradox: stateless JWTs cannot be revoked instantly out of the box.
3 Production Revocation Strategies:
- Short-Lived Access Tokens (5–15 mins) + Revocable Refresh Tokens: Keep access token lifetimes brief so revoked permissions propagate quickly upon the next refresh cycle.
- Distributed Denylist / Blocklist (Redis): When a user logs out or is banned, write their unique token ID (
jticlaim) into a Redis cluster with a TTL equal to the token's remaining lifetime. Middleware checks incomingjticlaims against Redis via sub-millisecond lookups. - User Security Stamps: Embed a
security_stamportoken_versionclaim. When a password changes, increment the user's version integer in a central cache. Downstream services reject tokens with mismatched version numbers.
* Self-Contained JWT: Carries all user scopes, identity claims, and roles inside the token itself.
- Pros: Zero database round-trips for resource APIs during validation. Maximum horizontal scalability.
- Cons: Revocation is complex; payload size increases HTTP header overhead.
- Pros: Instant revocation at the central authorization server. Zero sensitive data leakage on client devices.
- Cons: Requires resource APIs to perform Token Introspection (RFC 7662) over the network for every incoming request.
Standard Bearer tokens are like cash: whoever holds the token can spend it, making them vulnerable to token leakage, XSS theft, and man-in-the-middle attacks.
DPoP (RFC 9449): Cryptographically binds an access token to a specific client private key:
- The client generates an ephemeral public/private key pair.
- For every API request, the client creates and signs a
DPoPproof header containing the HTTP method, request URI, and a timestamp. - The API verifies that the access token was issued specifically to the public key in the DPoP header, and verifies the signature over the current request line.
In standard TLS, only the client verifies the server's identity using the server's SSL certificate.
Mutual TLS (mTLS): Both the client and the server possess X.509 digital certificates and cryptographically verify each other during the initial TLS handshake:
- The server validates the client's certificate against a trusted internal Certificate Authority (CA).
- Prevents spoofing, credential theft, and unauthorized network ingress.
- The industry standard for securing zero-trust east-west traffic within microservice meshes (e.g., Kubernetes service meshes using Envoy/Istio).
Storing access tokens in browser
localStorage or sessionStorage makes them vulnerable to exfiltration via Cross-Site Scripting (XSS).
The BFF Pattern:
- A lightweight backend layer (Node.js, .NET, Go) sits directly between the Single Page Application (SPA) and downstream microservices.
- The BFF manages the OAuth authentication flow, securely storing access and refresh tokens in server-side memory or encrypted sessions.
- The browser SPA communicates with the BFF using secure,
HttpOnly,SameSite=Strictcookies. - When forwarding calls to downstream APIs, the BFF strips the cookie, attaches the real
Authorization: Bearer <token>header, and proxies the call.
* OAuth Scopes (
scope: "read:orders write:orders"): Represent delegated permissions granted to a client application by the resource owner. Scopes define what the application is permitted to do on behalf of the user (e.g., "Allow this dashboard app to read your profile"). Scopes do not define user business roles.* RBAC / Permissions (
roles: ["BillingAdmin", "Auditor"]): Represent the inherent business permissions of the user within the enterprise.
Authorization Rule: Access is granted only if the user has the required business role AND the calling client application has been granted the corresponding delegated scope.
* Refresh Token Rotation: Every time a client exchanges a refresh token for a new access token, the authorization server invalidates the old refresh token and issues a brand new refresh token.
* Reuse Detection: If an attacker intercepts a refresh token and attempts to use it after the legitimate client has already rotated it:
- The authorization server detects that an already-invalidated refresh token was submitted.
- It recognizes an active breach, triggers an intrusion alert, and immediately revokes the entire token family (invalidating all active access and refresh tokens across all sessions for that user).
API Keys are suitable for server-to-server machine identification, but must follow strict cryptographic practices:
- Prefixing (The Stripe Pattern): Prefix keys with a readable namespace (e.g.,
sk_live_...orpk_test_...) so secret scanning tools (GitHub secret scanning) can identify leaked keys instantly. - Never Store in Plaintext: Treat API keys like passwords. Store only a cryptographic hash (SHA-256 or Argon2) in the database. During authentication, hash the incoming key and perform a constant-time comparison against the stored hash.
- Fine-Grained Scoping: Allow users to restrict API keys to specific IP CIDR blocks, specific HTTP verbs, and specific resource endpoints.
Ranked as the top threats in the OWASP API Security Top 10:
- BOLA (IDOR): The user is authenticated, but the API endpoint fails to verify whether the user actually owns the target entity:
Fix: Enforce database-level tenant/owner filtering on every query:GET /api/documents/105 # User A accesses User B's document by swapping IDs!WHERE id = @id AND tenant_id = @current_user_tenant. - BFLA: The API fails to verify administrative roles on sensitive endpoints:
DELETE /api/users/42 # Standard user executes admin-only action because the endpoint missed role checks
*
iss (Issuer) Validation: Verifies that the token was signed by the expected Identity Provider domain (e.g., https://auth.example.com). Prevents tokens signed by unauthorized or foreign authorization servers from being trusted.*
aud (Audience) Validation: Verifies that the token was minted specifically for this target resource API (e.g., aud: "https://api.payments.example.com").
The Danger of Missing Audience Checks: If Service A and Service B share the same Identity Provider, a malicious user could obtain a valid token for low-security Service A and forward that token to high-security Service B. If Service B skips audience validation, it accepts the unauthorized token.
Rate Limiting, Throttling & Abuse Prevention
- Fixed Window Counter: Tracks request counts within static time blocks (e.g., 100 requests per minute from 12:00 to 12:01).
Weakness: Susceptible to traffic burst spikes at window boundaries (e.g., 100 requests at 12:00:59 followed by 100 requests at 12:01:01 allows 200 requests within 2 seconds). - Sliding Window Log: Logs individual request timestamps in a sorted set (Redis ZSET). Calculates counts within the exact trailing window.
Trade-off: 100% accurate, but consumes massive memory storing every request timestamp. - Sliding Window Counter: Blends the count of the previous window with the current window using an interpolation formula. Memory-efficient ($O(1)$) and eliminates boundary burst spikes.
- Token Bucket: A bucket holds tokens up to a max capacity; tokens refill at a steady constant rate. Each request consumes a token. Accommodates controlled traffic bursts while maintaining long-term rate caps.
* Token Bucket: Tokens refill at a constant rate, but requests can be processed immediately in rapid bursts as long as tokens remain in the bucket. Focuses on capping average throughput while allowing short bursts.
* Leaky Bucket: Requests enter a FIFO queue (the bucket) and leak out to downstream handlers at a strictly constant, fixed processing rate regardless of incoming burst volume.
Primary Use Case: Leaky Bucket is ideal for smoothing out bursty traffic before feeding sensitive legacy systems that crash if execution rates fluctuate.
Modern APIs standardize on the IETF RFC draft headers:
HTTP/1.1 200 OK
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 18
RateLimit-Limit: Maximum quota units allowed in the current time window.RateLimit-Remaining: Remaining units available in the active window.RateLimit-Reset: Number of seconds until the current quota allocation resets.- When quota is breached, the server returns
429 Too Many Requestsaccompanied by aRetry-Afterheader.
Executing multiple separate Redis commands (e.g.,
INCR followed by EXPIRE) introduces race conditions where concurrent requests cause desynchronization or permanent keys without TTLs.
Atomic Redis Lua Script (Sliding Window Counter):
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local current = tonumber(redis.call('get', key) or "0")
if current + 1 > limit then
return 0 -- Rate limit exceeded
else
redis.call("INCRBY", key, 1)
if current == 0 then
redis.call("EXPIRE", key, ARGV[2]) -- Set TTL in seconds
end
return 1 -- Request approved
end
Because Redis executes Lua scripts atomically on a single thread, race conditions are completely eliminated across distributed API gateway nodes.
Selecting the rate-limiting partition key depends on the caller context:
- Client IP Address: Best for unauthenticated public endpoints (login, password reset, registration).
Risk: Corporate networks, mobile carrier NATs, and VPNs share single public IPs, causing thousands of legitimate distinct users to be throttled together. - API Key / Tenant ID: Best for B2B developer platforms. Enforces contract-level tier limits (e.g., Free Tier: 10 req/sec; Enterprise: 1,000 req/sec).
- Authenticated User ID (JWT
subclaim): Best for multi-device consumer applications, ensuring a user cannot bypass limits by hopping across cellular and Wi-Fi networks.
Rather than relying on a single rule, enterprise gateways apply multi-layered defense buckets:
Example:
- Layer 1 (Per-Second Burst Protection): Max 20 requests per second (prevents short-term micro-bursts from saturating CPU).
- Layer 2 (Per-Minute Quota): Max 500 requests per minute.
- Layer 3 (Daily Business Cap): Max 50,000 requests per day (enforces billing tiers).
- Layer 4 (Endpoint-Specific Throttle): Max 5 requests per minute on heavy endpoints like
POST /reports/exportorPOST /auth/login.
If an API uses static Fixed Windows resetting on the hour (e.g., at 12:00:00), thousands of throttled client scripts sleeping on timers will strike the API at the exact same millisecond when the clock strikes 12:00:00.
Mitigations:
- Switch from Fixed Windows to Token Bucket or Sliding Window algorithms so resets occur smoothly and continuously.
- Enforce Exponential Backoff with Full Jitter in client SDKs, adding randomized delays to retry windows: $$\text{Sleep} = \text{random}(0, \min(\text{MaxBackoff}, \text{Base} \times 2^{\text{attempt}}))$$
* Rate Limiting: Regulates the number of requests over a time duration (e.g., 100 requests per minute). It does not track whether previous requests have finished executing.
* Concurrency Limiting: Regulates the number of in-flight, simultaneously executing requests at any given millisecond (e.g., max 5 concurrent operations per tenant).
Why it is critical: A client executing 5 concurrent 30-second complex database reports can completely exhaust backend connection pools even while remaining well under a 100-req/min rate limit.
Load Shedding is an automated survival mechanism where a server deliberately drops a percentage of incoming requests to prevent total service collapse:
- The server monitors internal saturation metrics: active thread pool utilization, CPU load (> 90%), or response queue latency.
- When thresholds are breached, the server immediately rejects incoming non-critical requests (returning
503 Service Unavailable) without processing database logic. - Prioritizes critical traffic (e.g., checkout/payments) while shedding background syncs and analytics to keep the core platform operational.
If an API reads client IPs by blindly taking the leftmost value from the incoming
X-Forwarded-For header:
X-Forwarded-For: 1.1.1.1, 10.0.0.1
An attacker can forge arbitrary fake IPs in the request (X-Forwarded-For: 8.8.8.8), evading IP-based rate limiting entirely.
Security Rule: The reverse proxy or API Gateway must be configured with trusted proxy subnets. The gateway must overwrite or append to
X-Forwarded-For using the physical TCP socket address, and rate limiters must read only the verified client IP from trusted network boundaries.
Treating every request as 1 unit is flawed: fetching a cached item by ID costs 1ms of CPU, while an un-indexed full-text search costs 500ms of CPU.
Cost-Based Rate Limiting:
- Endpoints are assigned point costs based on backend complexity (e.g.,
GET /user= 1 point;POST /search= 10 points). - Users receive a budget of points per hour (e.g., 5,000 points/hr).
- Responses emit points consumed and points remaining via headers, ensuring heavy database workloads burn through quotas proportionally faster.
* Network Round-Trip Overhead: Checking Redis on every single API request adds 1–3ms of latency to every call.
* Single Point of Failure: If the central Redis cluster crashes, what happens to incoming API traffic?
Industry Best Practice (Fail-Open vs Fail-Closed): Most commercial gateways implement a Fail-Open policy: if Redis becomes unreachable, the rate limiter logs an alert and allows API traffic through unthrottled to avoid taking down production services over a telemetry cache failure.
* Local In-Memory Limiting (Per-Node): Tracks counts in application RAM. Nanosecond speed with zero network latency.
Weakness: Quotas are split across nodes. If you have 10 gateway instances and a 100-req/min limit, a client can theoretically issue 1,000 requests per minute if requests distribute evenly across all nodes.
* Hybrid Architecture: Use local memory for micro-second burst protection (e.g., max 50 req/sec per node) paired with an asynchronous background batch sync to central Redis for global hourly quotas.
When introducing rate limits onto an established legacy API:
Deploy rate limiters in Shadow / Dark Mode:
- The gateway tracks counters and evaluates thresholds.
- When a quota is breached, it logs metrics and triggers an internal alert, but does NOT block the request or return 429.
- Allows engineering teams to analyze traffic patterns, identify legitimate high-volume enterprise customers, and fine-tune quota thresholds before flipping to active enforcement.
Authentication endpoints (
/login) require specialized multi-dimensional abuse throttling:
- IP-based Limiting: Block or challenge IPs issuing > 10 failed login attempts per minute.
- Account-based Progressive Delays: If an individual account encounters repeated failed password attempts, introduce artificial sleep delays or temporary locks (regardless of which IP issued the attempt) to neutralize distributed botnets.
- CAPTCHA Integration: Trigger invisible Turnstile/reCAPTCHA challenges dynamically only when suspicious velocity thresholds are breached.
CORS, CSRF & Web Security Architecture
The Same-Origin Policy (SOP) is a fundamental browser security mechanism that restricts scripts executing in one document origin from reading or interacting with sensitive data from another origin.
Origin Definition: A combination of Scheme + Hostname + Port:
https://example.com:443andhttp://example.com:80→ Different (Scheme mismatch).https://api.example.comandhttps://example.com→ Different (Host/Subdomain mismatch).https://example.com/apiandhttps://example.com/login→ Same Origin.
evil.com) could make background fetch requests to bank.com/api/balance and read the user's private financial data.
CORS is an HTTP-header-based protocol that allows a server to explicitly declare which foreign origins are permitted to access its resources within a browser environment.
How it works:
- The browser automatically appends an
Origin: https://app.example.comheader to cross-origin requests. - The server checks the origin and returns an
Access-Control-Allow-Originheader. - The browser inspects the returned header: if the origin is authorized, the browser allows client-side JavaScript to read the response; if unauthorized, the browser blocks the response from JavaScript and logs a CORS violation error.
* Simple Request: Fired directly without a preflight check if it satisfies 3 strict conditions:
- Uses
GET,HEAD, orPOST. - Contains only CORS-safelisted headers:
Accept,Accept-Language,Content-Language,Content-Type. Content-Typeis restricted strictly to:application/x-www-form-urlencoded,multipart/form-data, ortext/plain.
application/json, custom headers (e.g., Authorization), or verbs like PUT/DELETE:
The browser must send an automatic OPTIONS request first to verify permissions before the real request is dispatched.
When an API handles authenticated user sessions via cookies, client certificates, or ambient authorization headers:
The browser specification strictly states: If
Access-Control-Allow-Credentials: true is present, the server CANNOT return a wildcard Access-Control-Allow-Origin: *.
Security Justification: Allowing wildcards with ambient credentials would allow any website on the internet to issue credentialed read requests to private user APIs and steal private data. The server must validate the incoming
Origin against an explicit whitelist and reflect that exact approved origin name back in the header.
Sending an
OPTIONS preflight check for every individual API call doubles network latency:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Max-Age: 86400
Access-Control-Max-Age: Caches the preflight approval inside the browser's local cache for a specified duration in seconds (e.g., 86400 = 24 hours). Subsequent requests skip the OPTIONS call entirely, eliminating the round-trip penalty.
CSRF occurs when a malicious site tricks a victim's browser into executing an unauthorized state-changing request on an API where the victim is authenticated via ambient credentials (Cookies). The browser automatically attaches cookies to cross-origin requests.
Why Bearer Token APIs are Immune: Browsers do NOT automatically attach
Authorization: Bearer <token> headers to cross-origin requests. The frontend JavaScript client must explicitly attach the header via script. A malicious form submission from evil.com cannot instruct the browser to inject an authorization header, neutralizing CSRF attacks completely.
The
SameSite attribute controls whether cookies are sent with cross-site requests:
SameSite=Strict: The cookie is never sent on cross-site requests (even when clicking a standard external link to the site). Maximum CSRF protection.SameSite=Lax(Modern Browser Default): The cookie is withheld on cross-site sub-requests (images, iframes, AJAXPOSTcalls), but is sent when a user navigates to the origin site via top-level link clicks (<a href>).SameSite=None; Secure: The cookie is sent with all cross-site requests. Mandatory for cross-origin embedded widgets/iframes. Requires HTTPS.
A stateless CSRF defense used when session cookies are required:
- Upon authentication, the server generates a cryptographically random token and sets it inside an accessible cookie (e.g.,
XSRF-TOKEN). - When making a mutation request (
POST,PUT), the frontend JavaScript reads the token from the cookie and inserts it into a custom request header (e.g.,X-XSRF-TOKEN). - The server compares the header value against the cookie value. Because SOP prevents a third-party site (
evil.com) from reading the origin's cookies, an attacker cannot forge the matching custom header.
Mass Assignment (CWE-915): Occurs when an API framework automatically binds raw incoming JSON properties directly to internal database entity models:
// Attacker injects unauthorized properties:
POST /users/update
{ "name": "Bob", "role": "admin", "isVerified": true }
If the controller passes the JSON body directly to db.Users.Update(user), the attacker elevates their privileges.
Defense: Always use strict Data Transfer Objects (DTOs) or input schemas (e.g., Zod, FluentValidation) with explicit field whitelisting.
SSRF (OWASP API Top 10): Occurs when an API accepts a client-supplied URL and makes an outbound HTTP request to it (e.g., user registers a webhook URL or profile image URL):
POST /webhooks/register
{ "url": "http://169.254.169.254/latest/meta-data/" }
The backend server fetches the URL, exposing cloud metadata services and cloud IAM credentials.
Defenses:
- Resolve DNS and reject loopback (
127.0.0.1), private RFC 1918 subnets (10.0.0.0/8,192.168.0.0/16), and link-local addresses (169.254.169.254). - Run webhook worker services in an isolated egress network sandbox.
By default in cross-origin requests, browser JavaScript can read only CORS-safelisted response headers:
Cache-Control, Content-Language, Content-Length, Content-Type, Expires, Last-Modified, Pragma.
If an API returns custom metadata headers (e.g.,
RateLimit-Remaining or X-Total-Count), client JavaScript cannot access them via fetch or axios unless the server explicitly white-lists them:
Access-Control-Expose-Headers: RateLimit-Remaining, X-Total-Count, Idempotency-Key
Mandatory production API security response headers:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload: Enforces HTTPS exclusively, preventing SSL-stripping man-in-the-middle attacks.X-Content-Type-Options: nosniff: Prevents MIME-sniffing exploits (stops browsers from executing an uploaded text file as JavaScript).X-Frame-Options: DENY: Prevents clickjacking by blocking the API from being embedded in<iframe>tags.Content-Security-Policy: default-src 'none': Disallows external resource loading if the API accidentally serves HTML/XML content.
A widespread security vulnerability where lazy developers attempt to support multiple origins:
// INSECURE: Blindly echoing back whatever Origin the client sends:
res.setHeader("Access-Control-Allow-Origin", req.headers.origin);
res.setHeader("Access-Control-Allow-Credentials", "true");
The Vulnerability: When evil.com issues an authenticated request with credentials, the server approves it, exposing the entire authenticated API to cross-origin data theft.
Fix: Check the origin against an immutable hardcoded list of verified enterprise domains.
CORS provides zero protection against non-browser clients:
- CORS is enforced purely by client web browser engines.
- Tools like
curl, Postman, Python scripts, backend microservices, and mobile apps do not enforce the Same-Origin Policy. - An attacker using Python or cURL can send arbitrary headers (or fake
Originheaders) and read API responses completely unimpeded by CORS configurations. - Rule: Never rely on CORS for API authorization or access control; use authentication tokens (OAuth/JWT) and API keys.
Historically, top-level JSON arrays (
[{"secret": "data"}]) could be hijacked by overriding the Array constructor prototype in malicious third-party <script> tags.
Modern Defenses:
- Always encapsulate JSON responses inside an outer object wrapper:
{ "data": [ ... ] } - Append a security prefix to the response stream (the Google/Angular pattern):
)]}',\n, forcing parsers to execute an explicit JSON parse rather than evaluating scripts.
HTTP Caching, ETags & Conditional Requests
The
Cache-Control header governs how browsers, CDNs, and forward/reverse proxies cache and serve representations:
public: The response may be stored by any cache, including shared intermediate caches, reverse proxies, and CDNs.private: The response is intended for a single user and must not be stored by shared caches (e.g., intermediate proxies or CDNs). Only the end-client browser cache may store it.no-cache: A cache may store the response, but it must revalidate with the origin server using conditional headers (If-None-Match) before serving it to a client. It does not mean "do not cache."no-store: The response must never be saved to persistent storage or memory caches anywhere along the network pipeline. Mandatory for sensitive data (PII, payment credentials).max-age=<seconds>: The maximum time in seconds the representation is considered fresh relative to the time of the request.s-maxage=<seconds>: Overridesmax-agespecifically for shared (public/CDN) caches; ignored by private browser caches.
An ETag is an opaque string identifier assigned by a web server to a specific version of a resource (typically an MD5/SHA-256 hash of the representation or a database concurrency version):
Conditional Revalidation Workflow:
- Initial request: The server returns
200 OKwith an ETag:HTTP/1.1 200 OK ETag: "686897696a7c876b7e" Cache-Control: no-cache - Subsequent request: The client sends back the stored ETag via the
If-None-Matchheader:GET /items/42 HTTP/1.1 If-None-Match: "686897696a7c876b7e" - The server evaluates the current hash of the resource:
- If unchanged, the server returns
304 Not Modifiedwith an empty body, saving massive bandwidth and serialization CPU. - If modified, the server returns
200 OKwith the new representation and new ETag.
- If unchanged, the server returns
* Strong ETag (
"abc-123"): Guarantees byte-for-byte physical equality across representations. If a single byte or header changes (e.g., alternate gzip compression level or white-space variation), the strong ETag changes. Strong ETags are required for Range requests (resumable byte downloads).* Weak ETag (
W/"abc-123"): Prefixed with W/. Guarantees semantic equivalence rather than byte-level equality. The underlying resource state is functionally identical, but formatting (e.g., date formats, attribute sorting, or compression codecs) may differ. Ideal for dynamic JSON representations generated across distributed microservices.
The Lost Update Problem occurs when two clients read the same resource and attempt to update it concurrently; the last write overwrites and erases the first write silently.
Optimistic Locking via
If-Match:
- Client reads an order: receives
ETag: "v1.0". - Client submits an update passing the ETag in the
If-Matchprecondition header:PUT /orders/105 HTTP/1.1 If-Match: "v1.0" Content-Type: application/json { "status": "shipped" } - If another user updated the order in the interim, the server's current ETag is
"v2.0". - The precondition evaluates to false. The server rejects the mutation immediately with
412 Precondition Failed, preventing data corruption.
stale-while-revalidate=<seconds> instructs caches to return a stale cached response immediately while asynchronously revalidating in the background:
Cache-Control: max-age=600, stale-while-revalidate=30
- 0 to 600s: Response is completely fresh; served from cache instantly.
- 601 to 630s: Response is stale, but served instantly to the user with zero latency. The cache simultaneously dispatches an asynchronous background fetch to the origin server to refresh the cache.
- > 630s: Stale window expired; the request blocks synchronously until the origin server responds.
stale-if-error=<seconds> allows an intermediate cache (like a CDN or reverse proxy) to serve expired stale data if the origin server responds with an error:
Cache-Control: max-age=3600, stale-if-error=86400
If the origin server crashes, encounters network partition, or returns 500 Internal Server Error, 502 Bad Gateway, 503, or 504, the CDN intercepts the failure and serves the stale representation for up to 24 hours (86,400 seconds) rather than surfacing error screens to users.
The
Vary header specifies which incoming request headers must be included in the cache key:
Vary: Accept-Encoding, Accept
The Danger of Vary: User-Agent:
Because thousands of unique browser user-agent strings exist across versions, operating systems, and mobile devices:
- Using
Vary: User-Agentshatters cache hit ratios. - A CDN must cache a separate copy of the payload for every unique browser string, reducing cache hit rates to near 0% and causing massive cache memory churn.
*
Last-Modified / If-Modified-Since: Relies on HTTP timestamps with 1-second resolution granularity.
Weaknesses: Cannot detect sub-second modifications, fails if system clocks across load-balanced servers drift, and triggers false invalidations if a file is touched without content mutating.
*
ETag / If-None-Match (Preferred): Content-based or version-based hashing. Immune to clock drift, supports sub-second precision, and validates exact semantic state.
Rule: When both are present on an incoming request,
If-None-Match takes precedence.
Cache-Tags (Surrogate-Keys): An advanced caching architecture used by enterprise CDNs (Fastly, Cloudflare) to invalidate related cached pages instantly:
- When serving an order, the origin server appends surrogate keys:
Surrogate-Key: user-42 order-105 customer-tier-gold - The CDN strips the header from the client response, but indexes the cached page under all three tags.
- When user 42 updates their profile, the backend issues an instant API purge call to the CDN:
POST /purge Surrogate-Key: user-42 - The CDN invalidates every cached endpoint tagged with
user-42globally within milliseconds, eliminating stale caches without waiting for TTL expirations.
While both directives force revalidation with the origin server under normal conditions:
max-age=0: Declares the representation as immediately expired. However, if paired withstale-if-errororstale-while-revalidate, an intermediate cache is permitted to serve the stale copy if the origin server is unreachable or offline.no-cache: Explicitly forbids caches from ever serving the response without successful origin revalidation first (unless strict disconnected mode headers are configured).
Cache-Control: public, max-age=31536000, immutable indicates that the representation's binary contents will never change during its lifetime:
- Normally, when a user presses the browser "Refresh" / F5 button, browsers bypass
max-ageand send conditionalIf-None-Matchrevalidation requests to the server. - The
immutabledirective instructs the browser that even during explicit user page refreshes, it must never issue revalidation requests. - Ideal for cache-busted, content-hashed static assets (e.g.,
bundle.a8b1c9.js,report-2026-Q1-v1.pdf).
The
Age response header is calculated and injected by intermediate caching proxies (CDNs) to indicate the number of seconds the representation has been resident in the cache since being generated by the origin server:
HTTP/1.1 200 OK
Cache-Control: public, max-age=3600
Age: 1200
Informs the client that the cached copy was fetched from the origin server 1,200 seconds ago, leaving $3600 - 1200 = 2400$ seconds of freshness remaining.
A Cache Stampede occurs when a heavily requested cached resource expires, and thousands of concurrent incoming requests simultaneously detect a cache miss and hit the origin database to regenerate the exact same representation at the same instant.
Mitigations:
- Request Coalescing (Collapsing): The reverse proxy/gateway allows only one single request to pass through to the origin server, while holding all other concurrent requests in queue to share the single generated response.
- Probabilistic Early Expiration (XFetch Algorithm): Background workers compute and refresh the cache before it hits its hard expiration date based on traffic velocity.
Under default HTTP rules, if a cache is disconnected from the network or the origin server is unreachable, the cache is technically permitted to serve an expired stale response to keep user navigation functional.
Adding
must-revalidate strictly revokes this fallback permission:
Cache-Control: max-age=300, must-revalidate
If the cache cannot revalidate with the origin server (e.g., due to network severance), it must return 504 Gateway Timeout rather than serving a stale representation. Mandatory for financial and authorization data.
By default, responses to requests carrying an
Authorization: Bearer ... header are considered non-cacheable by shared proxies and CDNs.
Safe Caching Architecture:
- Always specify
Cache-Control: privateto prevent private user data from entering public CDN edge caches. - If an authenticated response contains generic shared data (e.g., a shared catalog accessible to all logged-in members), explicitly declare
Cache-Control: public, s-maxage=...to override the default authorization caching block. - Never include user session tokens or authorization secrets in URLs.
Serialization, Compression & Payload Performance
* Gzip (
gzip): The universal baseline standard supported by 100% of HTTP clients. Moderate compression speed and ratios; high compatibility.* Brotli (
br): Optimized specifically for text payloads (JSON, HTML, CSS). Delivers 15% to 25% smaller file sizes than Gzip at equivalent compression levels.
Operational Note: Brotli Level 11 is CPU-intensive and should only be used for pre-compressed static assets; dynamic API JSON streams should use Brotli Level 4–6.
* Zstandard (
zstd): Developed by Meta (RFC 8878). Delivers compression ratios comparable to Brotli with 3x to 5x faster decompression speeds, making it the premier choice for internal high-throughput microservice payloads.
The N+1 API Problem occurs when a client fetches a list of $1$ collection resource, and is then forced to issue $N$ subsequent sequential HTTP requests to fetch child attributes for each individual item:
GET /orders (Returns 50 orders)
GET /customers/1
GET /customers/2 ... (50 separate round-trips!)
Architectural Fixes:
- Resource Embedding (
?include=customer): Eagerly loads and nests customer payloads directly inside the primary order objects. - Batch ID Endpoints: Support multi-ID lookups:
GET /customers?ids=1,2,3,4.... - BFF / GraphQL Layer: Use a Backend-For-Frontend gateway to orchestrate parallel backend reads and emit a single unified payload to the client.
At enterprise scale (tens of thousands of requests per second), JSON serialization becomes the primary CPU bottleneck:
- String Allocations: Converting in-memory objects into UTF-8 text strings forces massive ephemeral heap allocations, triggering frequent garbage collection sweeps.
- Reflection Overhead: Dynamic property reflection and key lookups consume significant CPU cycles.
- Mitigations: Use compile-time source-generated serializers (e.g.,
System.Text.JsonSource Generators in .NET, fast-json-stringify in Node.js) that generate direct memory byte serializers at compile time, bypassing runtime reflection completely.
The
Server-Timing header (W3C standard) communicates detailed backend performance metrics across network hops directly to browser DevTools and APM agents:
HTTP/1.1 200 OK
Server-Timing: db;dur=53.2;desc="Database Query", redis;dur=2.1, gateway;dur=5.0
Allows frontend monitoring tools to decompose total network latency into exact server-side segments (database, internal RPC, caching) without parsing custom JSON wrappers.
* HTTP/1.1: Browsers open up to 6 separate TCP connections per domain. On any single TCP socket, requests must be processed sequentially. If Request 1 stalls (e.g., a slow database query), Requests 2, 3, and 4 are blocked behind it (Application-layer Head-of-Line blocking).
* HTTP/2: Uses a single persistent TCP connection. Requests and responses are split into interleaved binary frames tagged with stream IDs. Dozens of concurrent API calls stream in parallel across the same socket; a stalled stream does not block adjacent streams.
While HTTP/2 solved application-level blocking, it introduced a single point of failure at the TCP transport layer:
Because all HTTP/2 streams share a single TCP connection, if even a single packet is dropped over a lossy cellular network, the TCP window halts processing for all concurrent streams until the lost packet is retransmitted.
HTTP/3 (QUIC over UDP): Implements independent, stream-isolated packet delivery. If a packet on Stream 3 is dropped, Stream 3 waits for retransmission, but Streams 1, 2, and 4 continue processing without delay.
HTTP 103 Early Hints allows the server to send preliminary response headers while the backend database is still calculating the final representation:
HTTP/1.1 103 Early Hints
Link: </css/main.css>; rel=preload; as=style, </api/user>; rel=preconnect
# Backend spends 300ms executing complex SQL queries...
HTTP/1.1 200 OK
Content-Type: text/html
The client browser pre-fetches stylesheets or opens TLS handshakes to API endpoints in parallel during server think time, shaving hundreds of milliseconds off overall page rendering.
Returning an array of 500,000 records as standard JSON (
[ {...}, {...} ]) requires the server to load all 500,000 objects into memory, buffer the complete string, and stream it at once.
NDJSON (Newline Delimited JSON,
application/x-ndjson):
Streams items separated by a newline character (\n):
{"id": 1, "name": "Item A"}
{"id": 2, "name": "Item B"}
Enables constant $O(1)$ memory consumption: the server reads a record from the database cursor, serializes it, flushes it to the socket, and reclaims memory immediately. The client processes each line asynchronously via streams without waiting for the full dataset.
Opening a new HTTPS connection requires: DNS lookup → TCP 3-way handshake → TLS cryptographic handshake (~3 to 4 network round trips before a single byte of data is sent).
Connection Pooling: Maintains a pool of open, warm, persistent TCP connections (via
Connection: keep-alive). Subsequent API requests reuse existing established sockets, eliminating handshake latency and reducing connection overhead from 200ms to 2ms.
Instantiating a new HTTP client per request (e.g.,
new HttpClient() in C# or unpooled agent in Node.js) closes the connection after every call.
The Problem: When a TCP connection closes, the operating system holds the ephemeral port in the
TIME_WAIT state for 120–240 seconds to ensure in-flight packets clear cleanly. Under high load, all ~65,000 outbound ports are exhausted, throwing:
SocketException: Only one usage of each socket address is normally permitted.
Fix: Use singleton, pooled HTTP clients (e.g.,
IHttpClientFactory, keep-alive http.Agent).
Pretty-printed JSON with indentation, line breaks, and whitespace accounts for 15% to 30% of total payload bytes over the wire.
Production API pipelines should always emit compact, minified JSON with all non-essential formatting characters stripped. Compression algorithms (Gzip/Brotli) reduce the impact, but raw unminified strings still waste CPU parsing cycles and memory allocation on client devices.
In microservices, repetitive request headers (e.g., large 1 KB JWT bearer tokens, cookies, user-agents) consume substantial bandwidth across millions of calls:
- HPACK (HTTP/2): Maintains a shared, stateful static and dynamic lookup table across a TCP connection. Once a header (e.g.,
Authorization) is transmitted, subsequent requests transmit only a small integer index pointing to the table entry, reducing header sizes by up to 85%. - QPACK (HTTP/3): Adapts header compression for out-of-order UDP streams, avoiding synchronization stalls.
Mobile architectural guidelines:
- Minimize Round Trips: Use BFF layers and resource embedding (
?include=...) to fetch screen views in a single call. - Sparse Fieldsets (
?fields=...): Exclude unnecessary fields to keep payload sizes small. - Aggressive Caching: Leverage
ETagsandstale-while-revalidateto render cached views instantly. - Resilient Network Protocols: Enforce HTTP/3 (QUIC) to maintain connections smoothly across cell-tower switches and Wi-Fi transitions.
* CPU-Bound Gateways: High CPU saturation (90%–100%) while network bandwidth and memory are low. Caused by heavy cryptographic TLS handshakes, repetitive JWT signature verifications, complex JSON schema transformations, or decompression loops.
* I/O-Bound Gateways: Low CPU usage (10%–20%), but elevated latency and high thread concurrency. Caused by upstream microservices responding slowly, database connection pool exhaustion, or downstream network socket stalls.
Traditional file streaming reads data from disk into kernel memory → copies it to user application memory → copies it back to kernel network socket memory (4 context switches, 2 CPU memory copies).
Zero-Copy I/O (Linux
sendfile system call):
Allows the operating system to transfer bytes directly from disk storage cache to the network interface card (NIC) buffer entirely within kernel space, bypassing application memory buffers completely. Reduces CPU utilization and memory bus overhead to near zero.
API Gateway & Backend-For-Frontend (BFF) Architecture
An API Gateway acts as the single reverse-proxy entry point for all external client traffic entering a microservice architecture.
Core Centralized Responsibilities:
- Routing & Reverse Proxying: Maps public URIs to private internal microservice instances.
- Authentication & Authorization: Validates OAuth tokens/JWTs at the edge before traffic hits internal networks.
- Rate Limiting & DDoS Mitigation: Enforces client throttling quotas and sheds malicious bursts.
- SSL/TLS Termination: Offloads cryptographic decryption overhead from backend application servers.
- Protocol Translation: Translates external REST/HTTP calls to internal gRPC or messaging queues.
- Cross-Cutting Concerns: Centralizes CORS, distributed tracing injection (Correlation IDs), and global access logging.
The Problem: A desktop web browser, a mobile smartphone app, and an IoT device require radically different data shapes, bandwidth profiles, and security mechanisms. A single generic API forces mobile apps to over-fetch data and execute multiple chatty round-trips.
The BFF Pattern: Deploy dedicated, independent gateway layers tailored specifically to each frontend interface:
- Web BFF: Handles cookie-based auth, serves desktop-optimized rich data sets.
- Mobile BFF: Handles token-based auth, compresses payloads, aggregates multiple microservice calls, and filters out unnecessary desktop fields.
Request Aggregation: The gateway intercepts a single high-level client request (e.g.,
GET /dashboard), fans out in parallel to query multiple backend microservices:
- Service A: User Profile
- Service B: Recent Orders
- Service C: Loyalty Points
When an upstream microservice fails or responds slowly, incoming requests queue up, exhausting worker threads and connection pools across the entire gateway, causing a cascading failure across unrelated services.
The Circuit Breaker (3 States):
- Closed (Normal): Requests pass through to the microservice. Failure rates are tracked.
- Open (Failing): If failure rates breach a threshold (e.g., > 50% errors), the circuit trips open. All incoming requests fail immediately (returning
503or a cached fallback) without touching the network, giving the downstream service time to recover. - Half-Open (Testing): After a timeout, a limited number of probe requests are allowed through. If they succeed, the circuit resets to Closed; if any fail, it trips back to Open.
* Pure Gateway Offloading (Perimeter Security): The gateway verifies the JWT, strips it, and passes plain headers (e.g.,
X-User-Id: 42) to internal services.
Weakness: Violates Zero-Trust; any compromised internal service or developer can spoof
X-User-Id headers.
* Zero-Trust Architecture (Recommended):
- Gateway authenticates the caller, validates rate limits, and strips untrusted headers.
- Gateway forwards the cryptographically signed JWT down to internal services.
- Internal microservices verify the token signature or communicate over mutual TLS (mTLS) with cryptographic service identities (SPIFFE/SPIRE).
Combines the security of opaque reference tokens with the performance of stateless JWTs:
- Public clients receive an opaque reference token (a random UUID string). No sensitive user claims or scopes are visible on client devices.
- When the client calls the API Gateway, the gateway introspects the opaque token via a local Redis cache.
- The gateway swaps the opaque token for a signed JWT and injects it into the
Authorization: Bearer <JWT>header before forwarding the call to internal microservices.
Named after the compartmentalized partitions of a ship's hull:
The gateway partitions its concurrent connection pools and thread allocations per upstream service:
- Payment Service Pool: Max 50 concurrent connections.
- Recommendation Service Pool: Max 20 concurrent connections.
Public web and mobile clients communicate using standard REST over HTTP/1.1 or HTTP/2 (JSON payloads). Internal microservices communicate over high-performance gRPC (Protocol Buffers):
- The gateway parses the incoming HTTP verb and JSON body.
- Using protobuf annotations (e.g.,
google.api.http), the gateway transcodes the JSON into binary protobuf frames. - It dispatches the call over persistent HTTP/2 gRPC channels to the target service, maps the binary response back to JSON, and returns it to the client.
In a distributed microservice mesh, a single user request can trigger dozens of internal RPC calls:
W3C Trace Context (RFC standard) ensures unbroken observability across service boundaries via headers:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
4bf92f...: Trace ID (globally unique across the entire multi-service transaction).00f067...: Parent / Span ID (identifies the specific sub-operation).- Every service propagates this header in outbound network calls, enabling tools like Jaeger, OpenTelemetry, and Datadog to visualize complete end-to-end flame graphs.
Standard HTTP caches match requests strictly by URI string identity.
Semantic Caching: Uses vector embeddings or normalized query ASTs to understand the meaning of requests:
- Common in AI/LLM API Gateways: if a user asks
"What is the capital of France?"and another asks"Tell me France's capital city", the semantic gateway computes vector similarity. - If similarity exceeds a threshold (> 0.95), it serves the cached response without invoking expensive backend AI models or databases.
* Blue-Green Routing: Two identical production environments exist (Blue = Active, Green = New Release). The gateway flips 100% of traffic from Blue to Green instantly via router configuration. Instant rollback if critical faults appear.
* Canary Deployments: The gateway routes a tiny percentage of live production traffic (e.g., 2%) to the new version, while sending 98% to the stable version:
- Canary selection can be based on random weights, specific user IDs, or custom beta-tester headers.
- Automated metrics analyzers monitor error ratios; if stable, traffic is progressively increased to 10%, 50%, and 100%.
Centralizing hundreds of services into a single monolithic gateway team/codebase introduces serious organizational and operational risks:
- Single Point of Failure: A configuration syntax error or memory leak in the gateway crashes the entire company's API infrastructure.
- Deployment Bottleneck: Dozens of development teams line up to update gateway routing rules, creating organizational gridlock.
- Modern Solution: Federated / Mesh Gateways (e.g., Kubernetes Gateway API) where individual product teams define their own route rules independently while platform teams manage global security policies.
HTTP Request Smuggling (CWE-444): Occurs when the front-end API gateway and back-end microservice disagree on request boundary boundaries:
If a request contains both
Content-Length and Transfer-Encoding: chunked headers:
- If the gateway uses
Content-Lengthand the backend usesTransfer-Encoding, the backend processes only part of the payload as Request 1. - The remaining unparsed payload bytes sit in the backend's network socket buffer and are prepended to the next incoming request from another user, allowing attackers to bypass auth or hijack user sessions.
In cloud-native container environments (Kubernetes, ECS), container IP addresses are dynamic, ephemeral, and scale continuously:
Service Discovery Integration:
- The gateway integrates with a Service Registry (Kubernetes CoreDNS, Consul, Eureka).
- When microservice pods scale up or down, the registry updates active IP endpoints.
- The gateway dynamically refreshes its internal load-balancing routing table (using algorithms like Round-Robin or Least-Connections) without requiring service restarts.
Follow the Google SRE Golden Signals:
- Latency: Measure p50, p95, and p99 response times. Decompose into Gateway Latency (overhead introduced by the proxy) vs Upstream Latency (backend execution time).
- Traffic: Total requests per second (RPS) broken down by route and client tier.
- Errors: Track 4xx (client errors) vs 5xx (server/gateway errors). Alert when 5xx rates breach 0.1%.
- Saturation: Track CPU utilization, worker thread pool exhaustion, open TCP connection counts, and active memory allocation limits.
Webhooks, Event Streaming & Real-Time APIs
* Standard API (Polling): The client repeatedly issues
GET requests to the server to check for state updates (e.g., checking order status every 5 seconds). Wasteful, high CPU overhead, and causes network latency.* Webhook (Reverse API / User-Defined HTTP Callback): An event-driven architecture where the consumer registers a callback URL with the provider. When an event occurs on the server, the server initiates an outbound HTTP POST request to the consumer's registered URL, delivering the event payload instantly.
To prevent attackers from sending fake webhook events to consumer endpoints:
- During webhook registration, the provider issues a shared cryptographic secret key to the consumer.
- When dispatching a webhook, the provider computes a HMAC-SHA256 signature across the raw request body payload and secret:
POST /webhook-consumer HTTP/1.1 X-Signature-256: t=1788739200,v1=a8b1c9...5d4e - The consumer reads the raw bytes, recomputes the HMAC-SHA256 signature using its shared secret, and performs a constant-time string comparison. If signatures match, the payload is authentic.
An attacker intercepting a valid signed webhook can replay the exact identical request repeatedly to duplicate operations on the consumer.
Timestamp Binding (The Stripe Pattern):
- The provider includes an explicit timestamp inside the signature header:
t=1788739200,v1=.... - The HMAC signature is calculated over
t + "." + payload. - When the consumer receives the event:
- It verifies that
Math.abs(currentTime - t) < 300(rejects any request older than 5 minutes). - It verifies the signature. Because the timestamp is included in the signed string, an attacker cannot modify
twithout invalidating the cryptographic signature.
- It verifies that
* At-Most-Once: The provider dispatches the webhook once; if the consumer crashes or network drops, the message is lost forever. Unacceptable for enterprise systems.
* At-Least-Once (Standard Production Model): The provider retries failed webhook dispatches until the consumer acknowledges receipt with a
2xx status code.
The Consequence: Due to network retries, duplicate webhook deliveries are guaranteed to happen. Consumers must implement Idempotency Checks (recording processed event IDs in a database table) before executing business logic.
Most webhook providers enforce a strict connection timeout (typically 5 to 10 seconds). If a consumer executes long-running operations (PDF generation, database analytics) synchronously inside the webhook handler, the provider times out and flags the endpoint as dead.
Asynchronous Ingestion Pattern:
- The consumer receives the webhook, verifies the HMAC signature, and writes the event immediately into an internal message queue (RabbitMQ, SQS, Kafka).
- The consumer returns
200 OKor202 Acceptedwithin 50 milliseconds. - A background worker service consumes the event from the internal queue asynchronously.
* Webhooks: Server-to-Server asynchronous notifications over standard HTTP POST. Best for third-party service integrations (GitHub PRs, Stripe payments).
* Server-Sent Events (SSE): Server-to-Client (Browser) unidirectional text streaming over a single long-lived HTTP connection using
text/event-stream. Supports automated reconnects. Best for stock tickers, live sports scores, and AI LLM chat token streaming.* WebSockets: Client-to-Server & Server-to-Client full-duplex, bidirectional communication over upgraded TCP sockets. Best for real-time multiplayer gaming, chat apps, and collaborative editing tools.
SSE uses standard HTTP with specific streaming headers:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Event Formatting: Data is streamed in plain UTF-8 text chunks separated by double newlines:
id: 101
event: priceUpdate
data: {"ticker": "MSFT", "price": 420.50}
id: 102
event: priceUpdate
data: {"ticker": "AAPL", "price": 230.10}
If the connection drops, the browser's native EventSource API automatically reconnects and sends the last received ID in the Last-Event-ID header, allowing the server to resume streaming without gaps.
When dispatching webhooks to customer servers that are offline or returning 500 errors:
- Exponential Backoff Retries: Retry the dispatch with increasing intervals (e.g., after 5s, 1m, 15m, 1h, 6h, 24h).
- Circuit Breaking: If a customer's endpoint fails 100% of calls over 48 hours, automatically disable the webhook and email the administrator to protect provider resources.
- Dead Letter Queue (DLQ): Once all retry attempts are exhausted, move the event into a persistent DLQ. Provide developers with a dashboard to inspect payloads, view stack traces, and trigger manual replays.
Historically, every SaaS vendor designed custom webhook JSON formats (Stripe looks different from GitHub, which looks different from Twilio).
CloudEvents (CNCF Specification): Standardizes event metadata fields across cloud platforms:
{
"specversion": "1.0",
"type": "com.example.order.created",
"source": "https://api.example.com/orders",
"id": "A234-1234-1234",
"time": "2026-09-01T12:00:00Z",
"datacontenttype": "application/json",
"data": {
"orderId": 105,
"amount": 99.00
}
}
When users can register arbitrary webhook callback URLs:
An attacker registers internal cloud addresses:
http://169.254.169.254/latest/meta-data/ or http://localhost:6379/. When an event fires, the provider's server sends a POST request to its own internal cloud metadata service, exposing IAM credentials.
Mandatory Defenses:
- Resolve DNS before dispatching; validate that the IP does not resolve to private subnets (
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1, or169.254.0.0/16). - Enforce DNS pinning to protect against DNS Rebinding (where a domain resolves to a public IP on registration, but flips to a private IP on dispatch).
- Execute webhook dispatch workers inside an isolated, egress-restricted network sandbox.
Used by platforms like Meta, Twitter, and Slack to verify that the user actually owns the registered callback URL before events begin streaming:
- The provider sends a
GETrequest to the newly registered webhook URL with a random challenge string:GET /webhook?crc_token=xyz123 HTTP/1.1 - The consumer must compute a signature over the token using their secret and return it inside a JSON payload within 3 seconds.
- If the response matches, the URL is verified and activated.
* Long Polling: The client sends an HTTP request. The server holds the request open until data is available. Once data is returned, the connection closes. The client must immediately open a brand new HTTP request.
Cost: Heavy overhead; every event requires a new TCP handshake, new HTTP headers, and repeated authentication validation.
* Server-Sent Events: The connection stays open indefinitely. Thousands of events are streamed over the single existing socket connection without reconnection overhead.
High-scale dispatcher architecture:
- Decouple with Message Brokers: Core API services emit domain events to Kafka or AWS SQS; never make synchronous webhook HTTP calls inside business transactions.
- Worker Fleet: A dedicated, auto-scaling fleet of lightweight worker microservices (Go/Rust/Node.js) consume events and execute outbound HTTP POST requests.
- Connection Pooling & Concurrency Clamping: Group events by destination domain; clamp outbound concurrency to any single customer host (e.g., max 20 concurrent sockets per customer domain) to avoid accidentally DDoS-ing customer infrastructure.
Sending every platform event to every registered endpoint overwhelms consumer networks with unwanted noise.
Event Subscriptions: Allow developers to subscribe strictly to specific event topics:
{
"url": "https://client.example.com/webhooks",
"events": ["payment.succeeded", "invoice.payment_failed"]
}
The dispatcher evaluates topic matching before queuing messages, ensuring clients receive only actionable business triggers.
Complete robust lifecycle integration:
- Outbound Call: Client sends
POST /chargeswithIdempotency-Key: idemp-987. - If network drops, client retries safely using the same key.
- Inbound Webhook: The payment provider dispatches a
payment.succeededwebhook withid: "evt_12345". - Consumer Ingestion: The consumer executes a database transaction:
INSERT INTO processed_events (event_id) VALUES ('evt_12345'); -- If unique constraint violation occurs -> Exit immediately with 200 OK! UPDATE orders SET status = 'PAID' WHERE order_id = 42; - Guarantees that regardless of how many times the API call or webhook event is retried, the order is fulfilled exactly once.
No comments:
Post a Comment