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.
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.
Request data comes from path parameters, query strings, headers, cookies, and parsed bodies. Give each source its own schema and merge only the validated result. Query values commonly arrive as strings or arrays, so coercion must be explicit: decide whether "01" is a valid integer representation, whether repeated keys are allowed, and how an empty string differs from omission or null.
Treat req.body as untrusted even after express.json parses it. Parsing proves JSON syntax, not object shape, key ownership, depth, or business meaning. Reject unknown keys for mutation commands when silent acceptance would hide client mistakes or mass-assignment risk. Build a new plain validated object rather than passing the original body into a model, ORM, template, or merge utility.
Set parser byte limits before validation and schema limits for strings, arrays, object depth, key count, and numeric range. A small compressed request can inflate into much more work, and a valid array with hundreds of thousands of items can exhaust CPU or database parameters. Use route-specific parsers or preflight controls for endpoints with genuinely different payload needs.
Normalize only when multiple forms have the same business meaning: trim surrounding whitespace for a code if spaces are never significant, canonicalize an email according to the application rule, and parse a date only with an accepted format and timezone policy. Do not strip punctuation globally or lowercase passwords, display names, signed values, or opaque identifiers.
Schema validation can compare fields such as start before end, but availability, uniqueness, balance, and workflow transition depend on current durable state. Enforce those rules in a transaction and back concurrency-sensitive conditions with constraints or conditional updates. A preflight uniqueness query improves the message but cannot prevent two requests from passing together.
Validate data received from databases, queues, configuration, and third-party APIs at the boundary where corruption would be costly. This is especially important during rolling schema changes. Serialize responses through an explicit resource schema so a newly added database column or internal flag cannot leak merely because an object is spread into JSON.
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
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.