Validate provider responses before returning an API result
Application and assignment
A shop asks a pricing provider for the cost of two items. Its own client expects a whole number of cents, so 200 means $2.00. A provider can return HTTP 200 while sending the wrong JSON type, such as the string "200". Transport success and a valid application result are different facts.
Use the supplied Python boundary model to follow one request through media-type, input, and provider-response validation. First predict which layer rejects each example. Then inspect the validators and extend one boundary. The AWS diagram explains where those checks belong, while this local exercise creates no gateway or function.
Contract and starting evidence
Constructed candidate brief: “Our quote API accepts quantity 2 and should
return integer total_cents: 200. The provider changes its response to string
\"200\". An API Gateway response model exists, but the client still receives
the string. Where will you enforce the contract, and how will you prove it?”
Prerequisites: JSON, HTTP status codes, and API ownership. Attempt the boundary diagram before opening boundary.py (download file, source below). This lab uses Python 3.11+ and no cloud resources or external dependencies.
Read the supplied code · boundary.py
"""Small application contract plus an explicit REST-Gateway boundary model."""
import json
class ContractError(ValueError):
pass
def request_body(value):
if not isinstance(value, dict) or set(value) != {"quantity"}:
raise ContractError("quantity required, no extra fields")
if type(value["quantity"]) is not int or not 1 <= value["quantity"] <= 10:
raise ContractError("quantity must be an integer from 1 to 10")
def response_body(value):
if not isinstance(value, dict) or set(value) != {"total_cents"}:
raise ContractError("total_cents required, no extra fields")
if type(value["total_cents"]) is not int or value["total_cents"] < 0:
raise ContractError("total_cents must be a nonnegative integer")
def edge_request(body, content_type, query, default_model=False):
"""Only the documented teaching subset; not an AWS emulator or JSON Schema engine."""
if not query.get("limit"):
return 400
if content_type == "application/json" or default_model:
try:
request_body(json.loads(body))
except (ValueError, TypeError):
return 400
return None
def envelope(status, body):
return {"statusCode": status, "headers": {"Content-Type": "application/json"}, "body": json.dumps(body)}
def application(body, content_type, query, provider):
if content_type != "application/json":
return envelope(415, {"error": "unsupported_media_type"})
try:
value = json.loads(body)
request_body(value)
limit = query.get("limit", "")
if not limit.isascii() or not limit.isdigit() or not 1 <= int(limit) <= 100:
raise ContractError("limit must be integer from 1 to 100")
except (ValueError, TypeError, AttributeError):
return envelope(400, {"error": "invalid_request"})
result = provider(value["quantity"])
try:
response_body(result)
except ContractError:
# Do not leak the malformed provider payload as an apparent success.
return envelope(502, {"error": "invalid_provider_response"})
return envelope(200, result)
def proxy_passthrough(result):
"""Envelope subset only; body schema is deliberately NOT checked here."""
if not isinstance(result, dict) or type(result.get("statusCode")) is not int or not isinstance(result.get("body"), str):
return 502, {"error": "invalid_integration_envelope"}
return result["statusCode"], json.loads(result["body"])
| Input | Expected application result | Enforcement |
|---|---|---|
quantity 2, limit 20; provider integer 200 |
200, {"total_cents":200} |
Request and response validators |
Same request; provider string "200" |
502, stable error body | Application response boundary |
limit banana present |
400 | App checks format/range; basic edge check only tests presence |
| Invalid JSON, matching model | 400 before provider | Configured request validation and application checks |
text/plain, no matching model |
Gateway body validation may be skipped; app 415 | Explicit media type policy |
Scope is REST API Gateway basic request validation and Lambda proxy response format, not HTTP API feature parity. The local gateway functions model only the listed distinctions; they are not an AWS emulator or a complete JSON Schema implementation. The application's typed validators execute real negative cases.
Baseline: a schema declaration does not execute a check
The model describes the body and can support SDK generation. With a proxy integration, a valid Lambda output envelope is passed through; declaring a method response model does not automatically validate the backend body. See method responses and Lambda proxy format.
Put an executable validator at the application boundary
- Separate request, provider response, application response, and integration envelope. Each may be structurally valid while the next contract is violated.
- Keep domain types explicit: integer minor units, not strings, floats, booleans, or an unbounded numeric coercion. Decide whether additional fields are allowed.
- Validate requests even when a caller reaches the backend through a different integration. Validate provider results before mapping them to success bodies.
- Return a stable 502 for malformed upstream data; do not label the customer's request invalid. Contract tests must fail on an injected malformed 200.
Run from the repository root:
python -m unittest discover -s curriculum/02-applications/01-backend/labs/api-contract -p 'test_*.py' -v
Five tests exercise the baseline malformed pass-through, corrected 502, valid
200, invalid/extra response fields, request schema, required parameter format,
unmatched content type, $default behavior, and malformed proxy envelope. No AWS
deployment is necessary to establish where the application's check lives.
Follow-up: another API flavor or integration path
Basic REST parameter validation checks presence/nonblank values, not parameter
type or format. Body validation needs a matching content-type model or $default;
passthrough settings are a separate choice. HTTP APIs have a different feature
set, so moving the same diagram to that flavor requires a fresh capability
review. See REST request validation.
Senior assessor checks: ask the candidate to locate the exact rejecting
function, then inject total_cents=true, an extra internal field, and malformed
limit. Require the provider call count to stay zero for invalid requests.
Lead: retain compatible consumers across contract versions; choose a rollout
gate and owner for provider violations. A response validator that silently
coerces everything into success hides a compatibility regression.
Follow-up: choose supported dependency infrastructure
As checked 2026-09-22, the App Mesh lifecycle notice announces support ending September 30, 2026. Treat it as a retirement/migration exercise, not a new-build recommendation. For an ECS deployment, evaluate Service Connect for its supported service discovery and connectivity behavior. Independently keep per-call deadlines, bounded retries, circuit breaking, and response validation explicit in the application; a healthy load-balancer target does not prove each downstream request meets its deadline or schema. Record behavior and compatibility gaps before a replacement, rather than assuming feature parity.
All linked pages are live official technical documentation with unverified publication age, accessed 2026-09-22. The lifecycle deadline is the announced fact; the access date is not a publication date or evidence of interview recency.
AWS route · Recovery extension