
Our approach to validating every API request before it touches business logic — Joi schemas, automatic normalization, multi-source validation (body, query, path), reusable validator factories, and the validation middleware that ties it all together.
Every API endpoint receives input from the outside world. Request bodies, query parameters, path parameters, headers — all of it is untrusted. A missing field causes a null reference error deep in the business logic. A string where a number is expected causes a type error in DynamoDB. An oversized payload causes a Lambda timeout. A malicious input causes a security vulnerability. The solution is validation at the edge — before the input reaches business logic, before it touches the database, before it can cause any damage. At TCTF, every Lambda function validates its input using a shared validation module built on Joi schemas with automatic normalization, multi-source support, and reusable validator factories. This article explains how it works.
In a Lambda function, the edge is the first line of the handler — the moment the API Gateway event arrives. Everything after that point assumes the input is valid. If the assumption is wrong, the error surfaces somewhere unexpected — a DynamoDB conditional check failure, a null pointer in a service method, a malformed response to the client.
Validating at the edge means checking every input before it enters the system. Is the email field present and formatted correctly? Is the page size a positive integer between 1 and 100? Is the request body under the size limit? Is the content type JSON? Is the HTTP method allowed for this endpoint?
The alternative — validating inside business logic — scatters validation across every function, every service method, every database call. It is inconsistent, incomplete, and impossible to audit. When validation is at the edge, it is in one place, it runs before anything else, and it is the same for every endpoint.
🛡️ Validate at the edge — the first line of the handler. Everything after assumes the input is valid. If validation is scattered across business logic, it is inconsistent and impossible to audit.

The validateRequest function is the entry point. It runs three checks in sequence before any schema validation happens.
First, HTTP method validation. Each endpoint declares which methods it accepts (GET, POST, PUT, DELETE). A POST to a GET-only endpoint is rejected immediately with a 405 Method Not Allowed. This catches misconfigured API Gateway routes and client errors before any processing.
Second, header validation. The function checks the total number of headers (default max: 50) and the total header size (default max: 8KB). This prevents header-stuffing attacks where a client sends thousands of headers to consume Lambda memory. Individual header values are not validated here — that is the schema's job.
Third, body validation. The function checks the body size (default max: 1MB) and the content type (default: application/json). An oversized body is rejected before JSON parsing — preventing the Lambda from spending time parsing a 100MB payload only to reject it later.
These three checks are structural — they validate the shape of the request, not the content. Schema validation handles the content.
After structural validation, the validateData function validates the request content against a Joi schema. Joi is a schema description language for JavaScript objects — you define the expected shape, types, constraints, and relationships, and Joi validates the input against them.
The function supports four validation sources: BODY (parsed JSON body), QUERY (query string parameters), PATH (path parameters from the URL), and HEADERS (request headers). The source determines where the data is extracted from the API Gateway event.
The schema defines every field: its type (string, number, boolean, array, object), whether it is required or optional, its constraints (min length, max length, pattern, enum values), and its relationships (field A is required when field B is present). Joi validates all of this in a single pass and returns either the validated data or a detailed error listing every violation.
The validated data is typed. If the schema describes a User object with userId (string), email (string), and age (number), the return type is { userId: string; email: string; age: number }. TypeScript enforces this at compile time — the handler receives typed, validated data, not an untyped any object.
📐Joi schemas define the expected shape, types, and constraints. The validated result is fully typed in TypeScript. The handler receives typed data, not untyped any.
Validation checks that data is correct. Normalization makes it consistent. The validation module supports automatic normalization as part of the validation pipeline — trim whitespace, lowercase emails, uppercase country codes — all before the data reaches business logic.
Normalization options are declared per-validator: which fields to trim, which to lowercase, which to uppercase. The normalization runs after Joi validation succeeds, so it only processes valid data.
For query and path parameters, the module applies sensible defaults automatically — trimming whitespace and lowercasing common fields like provider, type, and action. This prevents bugs where a query parameter has a trailing space or inconsistent casing.
Predefined normalization presets handle common patterns. The USER preset lowercases email and trims name fields. The SEARCH preset trims and lowercases search terms. Services can use presets or define custom normalization per endpoint.
The key principle: normalize once, at the edge. If every service normalizes email addresses independently, one will forget. If normalization happens in the validation pipeline, it is consistent across every endpoint.
✨Normalize once, at the edge. Trim whitespace, lowercase emails, uppercase country codes — all in the validation pipeline, before business logic sees the data.
Most endpoints validate the same way: parse the body, validate against a schema, normalize, return typed data or a 400 error. The createValidator and createValidationHandler factories encapsulate this pattern.
createValidator takes a Joi schema, a validation source, and normalization options, and returns a function that validates any API Gateway event against that schema. The returned function is reusable — define it once, use it in every handler that accepts that input shape.
createValidationHandler (aliased as createTypedValidator) goes further. It returns a function that validates the request and returns either a success result with typed data or a formatted error response ready to return to the client. The handler does not need to catch validation errors or format error responses — the factory handles it.
validateMultiple validates multiple sources in a single call — body, query, and path parameters all validated against their respective schemas in one operation. This is useful for endpoints that accept data from multiple sources (a POST with a body and query parameters).
The factory pattern means validation code is not duplicated across handlers. A user creation endpoint and a user update endpoint can share the same user schema with different required fields. A search endpoint and a list endpoint can share the same pagination schema. The schemas are defined once and composed into validators that are used everywhere.
Password validation goes beyond schema validation. The password validator module provides security-aware validation that checks against common attack patterns.
The validatePassword function checks length (minimum 8, maximum 128), character diversity (uppercase, lowercase, numbers, special characters), common patterns (sequential characters, repeated characters, keyboard patterns like qwerty), and known weak passwords (password123, admin, etc.).
The calculatePasswordStrength function scores passwords on a 0-100 scale based on length, diversity, entropy, and the absence of common patterns. The score maps to a human-readable strength: Very Weak, Weak, Moderate, Strong, Very Strong.
The validatePasswordWithContext function adds contextual checks — ensuring the password does not contain the user's email, name, or other personal information. This prevents the most common password weakness: using personal data that an attacker can easily guess.
Password validation is separate from request validation because it has different concerns — security scoring, pattern detection, and contextual checks that go beyond type and format validation. But it integrates with the same error handling architecture — a weak password throws a ValidationError with field-level details that the frontend can display.
The notification preferences module demonstrates how Joi schemas handle complex, nested validation.
User notification preferences include channel preferences (email, SMS, push — each with enabled/disabled and frequency settings), event type preferences (which notification types to receive), campaign subscriptions (which marketing campaigns to opt into), and quiet hours (time ranges when notifications should not be delivered, with timezone support).
Each of these is a separate Joi schema that validates independently. The unified preferences schema composes them into a single validation that checks the entire preferences object. The update schema validates partial updates (only the fields being changed). The patch schema validates individual field patches.
The extractValidationErrors function converts Joi's error format into a flat map of field paths to error messages — { 'channels.email.frequency': 'must be one of [immediate, daily, weekly]' }. This format is easy for the frontend to consume and display inline validation errors.
Helper functions like isValidTime24h and isValidTimezone provide specialized validation for time and timezone fields — ensuring quiet hours are valid 24-hour times and timezones are valid IANA identifiers.
This level of schema complexity would be unmanageable without a schema library. Joi makes it declarative — you describe what valid data looks like, and the library handles the validation logic.
📋Complex nested schemas: channel preferences, event types, campaign subscriptions, quiet hours with timezone support. Joi makes it declarative — describe valid data, the library validates.

Request validation is the first line of defense for every API endpoint. It catches invalid input before it can cause damage, normalizes data for consistency, and provides typed results for the handler. The shared validation module in tctf-utils ensures that every endpoint across all 34 services validates the same way — same Joi schemas, same normalization pipeline, same error format, same typed results. When validation is consistent, bugs are fewer, security is stronger, and the frontend always knows what error format to expect.
Never miss an edition
Subscribe to get TCTF newsletters delivered to your inbox.