# ADR-003: Standardized Input Validation using Middleware **Date**: 2025-12-12 **Status**: Accepted **Implemented**: 2026-01-07 ## Context Our Express route handlers currently perform manual validation of request parameters, queries, and bodies. This involves repetitive boilerplate code using `parseInt`, `isNaN`, and type checks like `Array.isArray`. This approach has several disadvantages: 1. **Code Duplication**: The same validation logic (e.g., checking for a valid integer ID) is repeated across many different routes. 2. **Cluttered Business Logic**: Route handlers are cluttered with validation code, obscuring their primary business logic. 3. **Inconsistent Error Messages**: Manual validation can lead to inconsistent error messages for similar validation failures across the API. 4. **Error-Prone**: It is easy to forget a validation check, leading to unexpected data types being passed to service and repository layers, which could cause runtime errors. ## Decision We will adopt a schema-based approach for input validation using the `zod` library and a custom Express middleware. 1. **Adopt `zod` for Schema Definition**: We will use `zod` to define clear, type-safe schemas for the `params`, `query`, and `body` of each API request. `zod` provides powerful and declarative validation rules and automatically infers TypeScript types. 2. **Create a Reusable Validation Middleware**: A generic `validateRequest(schema)` middleware will be created. This middleware will take a `zod` schema, parse the incoming request against it, and handle success and error cases. - On successful validation, the parsed and typed data will be attached to the `req` object (e.g., `req.body` will be replaced with the parsed body), and `next()` will be called. - On validation failure, the middleware will call `next()` with a custom `ValidationError` containing a structured list of issues, which `ADR-001`'s `errorHandler` can then format into a user-friendly `400 Bad Request` response. 3. **Refactor Routes**: All route handlers will be refactored to use this new middleware, removing all manual validation logic. 4. **(New) Resilient Type Inference**: To achieve full type safety, we will use **inline type assertions** with `z.infer`. This ensures the code inside the handler is fully typed and benefits from IntelliSense without creating complex utility types that conflict with Express's `RequestHandler` signature. ### Example Usage (Refined Pattern) ```typescript // In flyer.routes.ts import { z } from 'zod'; import { validateRequest } from '../middleware/validation'; // 1. Define the schema const getFlyerSchema = z.object({ params: z.object({ id: z.string().pipe(z.coerce.number().int().positive()), }), }); // 2. Infer the type from the schema for local use type GetFlyerRequest = z.infer; // 3. Apply the middleware and use an inline cast for the request router.get('/:id', validateRequest(getFlyerSchema), async (req, res, next) => { // Cast 'req' to the inferred type. // This provides full type safety for params, query, and body. const { params } = req as unknown as GetFlyerRequest; try { const flyer = await db.flyerRepo.getFlyerById(params.id); // params.id is 'number' res.json(flyer); } catch (error) { next(error); } }); ``` ## Consequences ### Positive **Reduced Complexity**: Avoids maintaining a complex ValidatedRequestHandler utility type that could conflict with Express or TypeScript upgrades. **Explicit Contracts**: By defining the Request type right next to the route, the contract for that endpoint is immediately visible. **Improved Readability**: Route handlers become much cleaner and focus exclusively on their core business logic. **Type Safety**: `zod` schemas provide strong compile-time and runtime type safety, reducing bugs. **Consistent and Detailed Errors**: The `errorHandler` can be configured to provide consistent, detailed validation error messages for all routes (e.g., "Query parameter 'limit' must be a positive integer"). **Robustness**: Prevents invalid data from ever reaching the service or database layers. ### Negative **Minor Verbosity**: Requires one extra line (type ... = z.infer<...>) and a controlled cast (as unknown as ...) within the handler function. **New Dependency**: Introduces `zod` as a new project dependency. **Learning Curve**: Developers need to learn the `zod` schema definition syntax. **Refactoring Effort**: Requires a one-time effort to create schemas and refactor all existing routes to use the `validateRequest` middleware. ## Implementation Details ### The `validateRequest` Middleware Located in `src/middleware/validation.middleware.ts`: ```typescript export const validateRequest = (schema: ZodObject) => async (req: Request, res: Response, next: NextFunction) => { try { const { params, query, body } = await schema.parseAsync({ params: req.params, query: req.query, body: req.body, }); // Merge parsed data back into request Object.keys(req.params).forEach((key) => delete req.params[key]); Object.assign(req.params, params); Object.keys(req.query).forEach((key) => delete req.query[key]); Object.assign(req.query, query); req.body = body; return next(); } catch (error) { if (error instanceof ZodError) { const validationIssues = error.issues.map((issue) => ({ ...issue, path: issue.path.map((p) => String(p)), })); return next(new ValidationError(validationIssues)); } return next(error); } }; ``` ### Common Zod Patterns ```typescript import { z } from 'zod'; import { requiredString } from '../utils/zodUtils'; // String that coerces to positive integer (for ID params) const idParam = z.string().pipe(z.coerce.number().int().positive()); // Pagination query params with defaults const paginationQuery = z.object({ limit: z.coerce.number().int().positive().max(100).default(20), offset: z.coerce.number().int().nonnegative().default(0), }); // Email with sanitization const emailSchema = z.string().trim().toLowerCase().email('A valid email is required.'); // Password with strength validation const passwordSchema = z .string() .trim() .min(8, 'Password must be at least 8 characters long.') .superRefine((password, ctx) => { const strength = validatePasswordStrength(password); if (!strength.isValid) ctx.addIssue({ code: 'custom', message: strength.feedback }); }); // Optional string that converts empty string to undefined const optionalString = z.preprocess( (val) => (val === '' ? undefined : val), z.string().trim().optional(), ); ``` ### Routes Using `validateRequest` All API routes use the validation middleware: | Router | Schemas Defined | Validated Endpoints | | ------------------------ | --------------- | -------------------------------------------------------------------------------- | | `auth.routes.ts` | 5 | `/register`, `/login`, `/forgot-password`, `/reset-password`, `/change-password` | | `user.routes.ts` | 4 | `/profile`, `/address`, `/preferences`, `/notifications` | | `flyer.routes.ts` | 6 | `GET /:id`, `GET /`, `GET /:id/items`, `DELETE /:id` | | `budget.routes.ts` | 5 | `/`, `/:id`, `/batch`, `/categories` | | `recipe.routes.ts` | 4 | `GET /`, `GET /:id`, `POST /`, `PATCH /:id` | | `admin.routes.ts` | 8 | Various admin endpoints | | `ai.routes.ts` | 3 | `/upload-and-process`, `/analyze`, `/jobs/:jobId/status` | | `gamification.routes.ts` | 3 | `/achievements`, `/leaderboard`, `/points` | ### Validation Error Response Format When validation fails, the `errorHandler` returns: ```json { "message": "The request data is invalid.", "errors": [ { "path": ["body", "email"], "message": "A valid email is required." }, { "path": ["body", "password"], "message": "Password must be at least 8 characters long." } ] } ``` HTTP Status: `400 Bad Request` ### Zod Utility Functions Located in `src/utils/zodUtils.ts`: ```typescript // String that rejects empty strings export const requiredString = (message?: string) => z.string().min(1, message || 'This field is required.'); // Number from string with validation export const numericString = z.string().pipe(z.coerce.number()); // Boolean from string ('true'/'false') export const booleanString = z.enum(['true', 'false']).transform((v) => v === 'true'); ``` ## Key Files - `src/middleware/validation.middleware.ts` - The `validateRequest` middleware - `src/services/db/errors.db.ts` - `ValidationError` class definition - `src/middleware/errorHandler.ts` - Error formatting for validation errors - `src/utils/zodUtils.ts` - Reusable Zod schema utilities ## Related ADRs - [ADR-001](./0001-standardized-error-handling.md) - Error handling for validation errors - [ADR-032](./0032-rate-limiting-strategy.md) - Rate limiting applied alongside validation