From 31a95f0d9b18a7afe801c1a883d053de88e2ac8e Mon Sep 17 00:00:00 2001 From: muzainat Date: Thu, 26 Mar 2026 10:31:00 +0100 Subject: [PATCH] feat: add request validation template --- README.md | 3 ++ docs/request-validation.md | 21 +++++++++ tooling/request-validation-template/README.md | 15 ++++++ .../example-validator.ts | 47 +++++++++++++++++++ tooling/request-validation-template/index.ts | 3 ++ .../validate-request.ts | 19 ++++++++ .../validation.errors.ts | 13 +++++ .../validation.types.ts | 12 +++++ 8 files changed, 133 insertions(+) create mode 100644 docs/request-validation.md create mode 100644 tooling/request-validation-template/README.md create mode 100644 tooling/request-validation-template/example-validator.ts create mode 100644 tooling/request-validation-template/index.ts create mode 100644 tooling/request-validation-template/validate-request.ts create mode 100644 tooling/request-validation-template/validation.errors.ts create mode 100644 tooling/request-validation-template/validation.types.ts diff --git a/README.md b/README.md index c032058..187dbc5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ # Timely Capsule App 🕰️ +## Reference templates + +The repository includes merge-safe request validation templates in `tooling/request-validation-template`. 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;