Production-grade guide to request validation patterns covering architecture patterns, implementation strategies, testing approaches, and operational best practices for enterprise engineering teams.
Request validation is the first line of defense for any backend service that accepts inbound traffic. It sits between the raw HTTP layer and your business logic, ensuring that malformed, malicious, or unexpected data never reaches processing paths where it could corrupt state, leak information, or trigger denial-of-service conditions. This page covers the practical ways to structure validation so that it is repeatable, observable, and resistant to the common gaps that appear in production systems.
The most frequent validation task is confirming that a request body conforms to the expected shape before any business logic runs. A concrete starting point is observing what happens when a client sends a payload that violates type expectations.
curl -s -X POST http://api.example.com/orders \
-H "Content-Type: application/json" \
-d '{"qty": -5, "id": "abc123"}' \
-w "\nHTTP Status: %{http_code}\n"
Typical output:
HTTP Status: 400
The 400 status indicates that the server rejected the request before it reached routing or processing. Many frameworks return 422 Unprocessable Entity when the payload shape is correct but values are invalid (e.g., a negative quantity where only positive integers are allowed). The distinction matters: 400 often signals a malformed request format, while 422 signals a valid format with invalid content.
A minimal validation function that can be dropped into any language runtime without external dependencies might look like this:
def validate_payload(data, rules):
errors = []
for field, rule in rules.items():
if rule.get("required") and field not in data:
errors.append(f"{field} is required")
continue
if field in data and "type" in rule:
expected = rule["type"]
if expected == "integer" and not isinstance(data[field], int):
errors.append(f"{field} must be integer")
elif expected == "string" and not isinstance(data[field], str):
errors.append(f"{field} must be string")
elif expected == "number" and not isinstance(data[field], (int, float)):
errors.append(f"{field} must be number")
if errors:
return False, "; ".join(errors)
return True, None
When called with {"qty": -5} and rules { "qty": {"required": true, "type": "integer"}, "id": {"required": true, "type": "string"} }, the function returns (False, "qty must be integer"). The caller can then compose a 422 response with those details, or a 400 if the payload itself cannot be parsed.
Beyond manual field checks, many teams centralize validation logic in a schema store. A common pattern involves sending the raw body to a validation endpoint that compares it against a stored JSON Schema or OpenAPI fragment. The curl interface to such a service typically follows this form:
curl -s -X POST http://validate.example.com/check \
-H "Content-Type: application/json" \
-d @payload.json \
-o response.body \
-w "HTTP Status: %{http_code}\n"
If the payload conforms, the service returns 200 OK; if it does not, it returns 400 Bad Request with a JSON body listing each violated keyword (e.g., {"error": "missing required field \"email\""}). The -o response.body flag captures that diagnostic payload for logging or alerting.
A real jq one-liner can validate a local file against a minimal schema expressed as a filter:
jq 'if has("email") and (.email | test("@")) then true else error("validation failed") end' <<< '{"email": "[email protected]"}'
Output:
true
Attempting the same against a malformed payload:
jq 'if has("email") and (.email | test("@")) then true else error("validation failed") end' <<< '{"email": "invalid"}'
Produces:
jq: error: Regex: Compilation error(s) at :1.1-1.13: quantifier operand must be a string or a character class for test; ...
If you need more help, see: https://jqlang.github.io/manual/
This demonstrates how jq can serve as a lightweight on‑demand validator in scripts or CI steps, without pulling in a dedicated schema‑validation library.
When a request slips past application‑level validation and reaches the database, a downstream constraint violation may surface as a 23505 (unique constraint violation) or 22P02 (invalid input syntax for type) error, depending on the PostgreSQL error class. These are not validation failures in the HTTP sense, but they underscore the importance of aligning application‑level checks with database schemas.
Body shape is only one vector. Headers and auth tokens are equally prone to omission or corruption.
curl -s -I http://api.example.com/data
Typical output:
HTTP/1.1 415 Unsupported Media Type
Content-Type: text/plain
Content-Length: 19
Date: Tue, 17 Sep 2025 12:34:56 GMT
Server: example/1.0
A 415 response indicates that the Content-Type header does not match what the endpoint expects. Omitting the header entirely, sending text/plain for a JSON endpoint, or providing application/xml when only JSON is supported will all trigger this rejection.
For authentication‑adjacent validation, the Authorization header is the most common entry point:
curl -s -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9" \
http://api.example.com/protected
If the token is malformed, expired, or signed with a key the server does not recognize, the backend typically returns 401 Unauthorized with a WWW-Authenticate challenge, or 403 Forbidden if the token is valid but the associated principal lacks the required scope. A frequent operational surprise is a 200 OK response when a reverse proxy strips the Authorization header before the request reaches the app, effectively bypassing the check entirely.
Several patterns lull operators into a false sense of security.
/^.+@.+\..+$/. This accepts a@b or [email protected] and rejects perfectly valid addresses with quoted local parts or subdomain‑rich domains. A JSON Schema format: "email" keyword is similarly limited—it follows the ABNF from RFC 5322 but does not guarantee deliverability. The practical outcome is that malformed addresses pass validation, only to cause bounce‑back errors or downstream data‑quality reports months later.client_max_body_size directive (in Nginx, Apache, or API‑gateway configs) and a corresponding application‑level body_limit flag. If the proxy enforces a 1 MiB limit but the app accepts bodies up to 10 MiB, a client can send a 2 MiB payload that the proxy forwards, the app validates, processes, and only then hits a memory‑exhaustion error or an internal 500. Conversely, if the proxy drops the connection at 1 MiB but the app logs the request as “received,” the logs become inconsistent. Aligning both boundaries—and documenting the alignment—is a frequent post‑mortant fix.400 vs. 422 semantics: 400 means the server cannot parse the request at all (e.g., invalid HTTP syntax, missing Content-Type, unparseable JSON). 422 means the server parsed the request successfully but the representation is semantically inappropriate for the resource. Returning 400 for a valid JSON body with a missing required field is a misclassification that breaks client error‑handling logic expecting 422 for business‑rule violations.Content-Type: application/json is present does not guarantee the body is valid JSON. A client can send application/json with plain‑text content, or application/octet-stream with JSON‑encoded data. The safe pattern is to attempt parsing the body regardless of the header, then validate the parsed object, and return 415 only if parsing itself fails.max_request_size or request_body_limit flag, while application frameworks expose a counterpart. If both are set, the effective limit is the stricter of the two, but which one “wins” depends on the order of processing. Documenting the effective limit—measured in bytes, not “kilobytes” or “megabytes” ambiguously—prevents misconfiguration.422 for valid‑format, invalid‑content payloads; return 400 for truly malformed requests (parse failures, missing headers, wrong content‑type).This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy