3.7 KiB
ADR-003: Standardized Input Validation using Middleware
Date: 2025-12-12
Status: Proposed
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:
- Code Duplication: The same validation logic (e.g., checking for a valid integer ID) is repeated across many different routes.
- Cluttered Business Logic: Route handlers are cluttered with validation code, obscuring their primary business logic.
- Inconsistent Error Messages: Manual validation can lead to inconsistent error messages for similar validation failures across the API.
- 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.
-
Adopt
zodfor Schema Definition: We will usezodto define clear, type-safe schemas for theparams,query, andbodyof each API request.zodprovides powerful and declarative validation rules and automatically infers TypeScript types. -
Create a Reusable Validation Middleware: A generic
validateRequest(schema)middleware will be created. This middleware will take azodschema, 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
reqobject (e.g.,req.bodywill be replaced with the parsed body), andnext()will be called. - On validation failure, the middleware will call
next()with a customValidationErrorcontaining a structured list of issues, whichADR-001'serrorHandlercan then format into a user-friendly400 Bad Requestresponse.
- On successful validation, the parsed and typed data will be attached to the
-
Refactor Routes: All route handlers will be refactored to use this new middleware, removing all manual validation logic.
Example Usage
// In flyer.routes.ts
import { z } from 'zod';
import { validateRequest } from '../middleware/validation';
// Define the schema for the GET /:id route
const getFlyerSchema = z.object({
params: z.object({
id: z.string().pipe(z.coerce.number().int().positive()),
}),
});
// Apply the middleware to the route
router.get('/:id', validateRequest(getFlyerSchema), async (req, res, next) => {
try {
// req.params.id is now guaranteed to be a positive number
const flyerId = req.params.id;
const flyer = await db.flyerRepo.getFlyerById(flyerId);
res.json(flyer);
} catch (error) {
next(error);
}
});
Consequences
Positive
DRY and Declarative: Validation logic is defined once in a schema and removed from route handlers.
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
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.