Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions docs/request-validation.md
Original file line number Diff line number Diff line change
@@ -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<T>` 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.
15 changes: 15 additions & 0 deletions tooling/request-validation-template/README.md
Original file line number Diff line number Diff line change
@@ -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.
47 changes: 47 additions & 0 deletions tooling/request-validation-template/example-validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { ValidationResult } from "./validation.types";

export interface CreateCapsuleShape {
title: string;
unlockDate: string;
}

export const validateCreateCapsule = (
value: unknown
): ValidationResult<CreateCapsuleShape> => {
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<string, unknown>;

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
}
};
};
3 changes: 3 additions & 0 deletions tooling/request-validation-template/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./validation.types";
export * from "./validation.errors";
export * from "./validate-request";
19 changes: 19 additions & 0 deletions tooling/request-validation-template/validate-request.ts
Original file line number Diff line number Diff line change
@@ -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 =
<T>(target: RequestTarget, validator: Validator<T>) =>
(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();
};
13 changes: 13 additions & 0 deletions tooling/request-validation-template/validation.errors.ts
Original file line number Diff line number Diff line change
@@ -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
});
12 changes: 12 additions & 0 deletions tooling/request-validation-template/validation.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export interface ValidationIssue {
field: string;
message: string;
}

export interface ValidationResult<T> {
success: boolean;
data?: T;
issues?: ValidationIssue[];
}

export type Validator<T> = (value: unknown) => ValidationResult<T>;
Loading