Validation is one of the fastest ways to make an API feel trustworthy.
If the request boundary accepts anything, the rest of the application is forced to deal with avoidable chaos later.
Beginners often think validation is only about required fields. Professionals know it also protects business rules, debugging clarity, and security posture.
Sanitization belongs nearby because accepted input should be normalized before it spreads further into the system.
At first, validation looks like a checklist: field present, email shaped correctly, number inside range. But its real value is bigger. Validation protects the business from impossible states and protects developers from guessing what kind of data reached a service function.
When bad input is rejected early, the rest of the code gets simpler. Services can assume more. Logs become clearer. Client errors become easier to explain.
Sanitization is about cleaning and normalizing data that you do decide to accept. That might mean trimming spaces, normalizing casing, stripping fields the client should not control, or transforming values into a safer internal shape.
Without sanitization, two values that mean the same thing may be stored inconsistently. That creates subtle bugs in search, comparison, deduplication, and reporting.
Validation answers whether input has the expected shape and meaning. Define a schema for body, path parameters, and query parameters before the controller runs. Check required fields, types, allowed values, formats, length limits, numeric ranges, and cross-field rules. Reject unknown fields for sensitive writes so clients cannot mass-assign internal properties.
Parsing and validation are separate. JSON parsing can fail before a schema sees the body. Query parameters arrive as strings and should be converted deliberately rather than through surprising truthy rules. Normalize email casing or whitespace only when the business meaning allows it, and preserve the original value when auditing matters.
The validation middleware should produce a typed or trusted value for downstream code. Store it in a clear request property or pass it directly to the controller. Services should not repeat HTTP parsing, but important domain invariants still belong in the service or database because requests are not the only way business operations may run.
A strong validation layer does not only reject input. It explains why the input failed in a way the client can act on. Generic error messages waste time for frontend developers and frustrate users.
Professional teams also keep validation errors structured and consistent so logs, dashboards, and consuming apps can reason about them predictably.
Validate a registration payload with explicit types, length limits, allowed values, and cross-field rules before it reaches the service. Return field-level errors without echoing secrets.
Work through this as a controlled engineering exercise rather than a copy-and-paste demo. State the expected result before running anything, keep the input small enough to inspect, and record the important intermediate state. That makes the lesson explain not only what to type, but why the result is trustworthy.
Coercion can turn surprising strings into booleans or numbers, and sanitizing SQL input is not a substitute for parameterized queries. Unknown fields can also become mass-assignment vulnerabilities.
Verification must use evidence that matches the concept. Test empty, oversized, malformed, duplicate, and unknown fields; assert the service is not called when validation fails. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.
Sanitization should be context-specific. Parameterized SQL protects database queries; HTML escaping protects rendering; URL validation protects outbound requests. A generic function that removes suspicious characters can corrupt valid names while failing to prevent injection in the actual sink. Validate structure, then encode or parameterize at the point of use.
Apply body-size limits, collection limits, string limits, recursion limits, and request timeouts before expensive processing. Validate uploaded file signatures rather than trusting extensions. For URLs, control schemes, DNS resolution, redirects, and private network ranges to reduce server-side request forgery risk.
Return a stable error contract with a machine-readable code, human message, field path, and correlation ID. Do not expose stack traces, SQL details, tokens, or the complete rejected secret. Track validation failure rates by endpoint and rule because sudden changes can reveal broken clients, abuse, or an incompatible deployment.
This flow keeps application logic calmer downstream.
Receive payload -> validate shape and rules -> sanitize accepted fields -> reject or continue -> call service with a trusted input object
Adapt this focused example to a disposable local environment and inspect every result before expanding it.
const schema = z.object({
email: z.string().email().max(254),
password: z.string().min(12).max(128),
role: z.enum(['user', 'editor'])
}).strict();
Parse each request source and pass only trusted values onward.
const createUser = z.object({
email: z.string().email().max(254).transform(v => v.toLowerCase()),
password: z.string().min(12).max(128),
role: z.enum(['user', 'editor']).default('user')
}).strict();
const result = createUser.safeParse(req.body);
if (!result.success) return next(new ValidationError(result.error));
req.validatedBody = result.data;
next();
Map library-specific errors into an API-owned contract.
{
"error": {
"code": "VALIDATION_FAILED",
"message": "One or more fields are invalid.",
"requestId": "req_01J...",
"fields": [
{"path": "email", "code": "invalid_email"}
]
}
}
They are different concerns, but in practice they are often implemented together near the request boundary.
Basic request-shape validation usually belongs close to the request boundary, while deeper business-rule validation may also exist inside the service layer.
Explore 500+ free tutorials across 20+ languages and frameworks.