diff --git a/docs/request-validation.md b/docs/request-validation.md new file mode 100644 index 0000000..e50d51d --- /dev/null +++ b/docs/request-validation.md @@ -0,0 +1,21 @@ +# Request Validation + +The repository includes a reference validation toolkit for future Express routes. + +## Goals + +- normalize request validation behavior +- return a consistent `400` error payload +- keep validators testable outside route files + +## Current location + +`tooling/request-validation-template` + +## Included pattern + +- `Validator` for schema-like parsing +- `validateRequest()` middleware wrapper +- shared validation error payload formatting + +This implementation is intentionally dependency-light so it can merge before any framework or package decisions are finalized. diff --git a/tooling/request-validation-template/README.md b/tooling/request-validation-template/README.md new file mode 100644 index 0000000..8790e1b --- /dev/null +++ b/tooling/request-validation-template/README.md @@ -0,0 +1,15 @@ +# Request Validation Template + +This template provides a lightweight validation pattern for future Express routes. + +## Included pieces + +- schema-like validator helpers +- request payload guards +- normalized error formatting + +## Intended usage + +Copy these files into the API app when route handlers are added. + +The template avoids coupling to a specific validation library so it can merge safely before the API package exists. diff --git a/tooling/request-validation-template/example-validator.ts b/tooling/request-validation-template/example-validator.ts new file mode 100644 index 0000000..3861283 --- /dev/null +++ b/tooling/request-validation-template/example-validator.ts @@ -0,0 +1,47 @@ +import type { ValidationResult } from "./validation.types"; + +export interface CreateCapsuleShape { + title: string; + unlockDate: string; +} + +export const validateCreateCapsule = ( + value: unknown +): ValidationResult => { + const issues = []; + + if (typeof value !== "object" || value === null) { + return { + success: false, + issues: [{ field: "body", message: "Request body must be an object" }] + }; + } + + const record = value as Record; + + if (typeof record.title !== "string" || record.title.trim().length < 3) { + issues.push({ + field: "title", + message: "Title must be at least 3 characters long" + }); + } + + if (typeof record.unlockDate !== "string" || Number.isNaN(Date.parse(record.unlockDate))) { + issues.push({ + field: "unlockDate", + message: "Unlock date must be a valid ISO date string" + }); + } + + if (issues.length > 0) { + return { success: false, issues }; + } + + return { + success: true, + data: { + title: record.title.trim(), + unlockDate: record.unlockDate + } + }; +}; diff --git a/tooling/request-validation-template/index.ts b/tooling/request-validation-template/index.ts new file mode 100644 index 0000000..283f35b --- /dev/null +++ b/tooling/request-validation-template/index.ts @@ -0,0 +1,3 @@ +export * from "./validation.types"; +export * from "./validation.errors"; +export * from "./validate-request"; diff --git a/tooling/request-validation-template/validate-request.ts b/tooling/request-validation-template/validate-request.ts new file mode 100644 index 0000000..693042d --- /dev/null +++ b/tooling/request-validation-template/validate-request.ts @@ -0,0 +1,19 @@ +import type { NextFunction, Request, Response } from "express"; +import { toValidationErrorPayload } from "./validation.errors"; +import type { Validator } from "./validation.types"; + +type RequestTarget = "body" | "params" | "query"; + +export const validateRequest = + (target: RequestTarget, validator: Validator) => + (request: Request, response: Response, next: NextFunction) => { + const result = validator(request[target]); + + if (!result.success) { + response.status(400).json(toValidationErrorPayload(result.issues ?? [])); + return; + } + + request[target] = result.data as Request[typeof target]; + next(); + }; diff --git a/tooling/request-validation-template/validation.errors.ts b/tooling/request-validation-template/validation.errors.ts new file mode 100644 index 0000000..e394d15 --- /dev/null +++ b/tooling/request-validation-template/validation.errors.ts @@ -0,0 +1,13 @@ +import type { ValidationIssue } from "./validation.types"; + +export interface ValidationErrorPayload { + error: "validation_error"; + issues: ValidationIssue[]; +} + +export const toValidationErrorPayload = ( + issues: ValidationIssue[] +): ValidationErrorPayload => ({ + error: "validation_error", + issues +}); diff --git a/tooling/request-validation-template/validation.types.ts b/tooling/request-validation-template/validation.types.ts new file mode 100644 index 0000000..079526c --- /dev/null +++ b/tooling/request-validation-template/validation.types.ts @@ -0,0 +1,12 @@ +export interface ValidationIssue { + field: string; + message: string; +} + +export interface ValidationResult { + success: boolean; + data?: T; + issues?: ValidationIssue[]; +} + +export type Validator = (value: unknown) => ValidationResult;