Action

Type

Resolved On

API Validation Utilities refactoring - - -

API Validation Utilities

Overview

All API routes repeat similar form validation patterns, leading to code duplication and inconsistent validation approaches across the application.

Problem

Each API route repeats validation logic like this example from api/new/goal.ts:

if (!owner) {
  return new Response(JSON.stringify({ error: "Owner is required" }), {
    status: 400,
    headers: { "Content-Type": "application/json" },
  });
}
if (!year || isNaN(year)) {
  return new Response(JSON.stringify({ error: "Valid year is required" }), {
    status: 400,
    headers: { "Content-Type": "application/json" },
  });
}
if (!month || isNaN(month) || month < 1 || month > 12) {
  return new Response(
    JSON.stringify({ error: "Valid month is required (1-12)" }),
    { status: 400, headers: { "Content-Type": "application/json" } }
  );
}

Affected Files

All API routes in src/pages/api/ repeat similar validation patterns:

  • api/new/goal.ts
  • api/new/backlog.ts
  • api/open/*.ts
  • api/close/*.ts
  • api/update/*.ts

Solution

Create a validation utility module at src/lib/validation.ts with common validation functions.

Proposed API

// Validation helpers
export function required(value: any, fieldName: string): string | null
export function validNumber(value: any, fieldName: string): string | null
export function validRange(value: number, min: number, max: number, fieldName: string): string | null

// FormData extractors with validation
export function getString(formData: FormData, field: string): { value: string; error: string | null }
export function getNumber(formData: FormData, field: string): { value: number; error: string | null }
export function getNumberInRange(formData: FormData, field: string, min: number, max: number): { value: number; error: string | null }

// Composite validator
export class FormValidator {
  private errors: string[] = []

  required(value: any, fieldName: string): this
  number(value: any, fieldName: string): this
  range(value: number, min: number, max: number, fieldName: string): this

  isValid(): boolean
  getErrors(): string[]
}

Usage Example

// Before
const year = Number(formData.get("year"));
if (!year || isNaN(year)) {
  return badRequest("Valid year is required");
}

// After - Simple approach
const yearResult = getNumber(formData, "year");
if (yearResult.error) {
  return badRequest(yearResult.error);
}

// After - Fluent API approach
const validator = new FormValidator();
validator
  .required(formData.get("owner"), "Owner")
  .number(formData.get("year"), "Year")
  .number(formData.get("month"), "Month")
  .range(Number(formData.get("level")), 0, 3, "Level");

if (!validator.isValid()) {
  return badRequest(validator.getErrors().join(", "));
}

Benefits

  • Reduced Duplication: Common validations defined once
  • Consistency: Same validation rules across all endpoints
  • Readability: API routes focus on business logic, not validation
  • Maintainability: Update validation rules in one place
  • Type Safety: Better TypeScript integration with return types

Implementation Notes

  • Consider integrating with the API response utility for consistent error formatting
  • Could add more validators: email, URL, date format, etc.
  • Optional: Add i18n support for error messages
  • src/pages/api/new/goal.ts
  • src/pages/api/new/backlog.ts
  • src/pages/api/open/*.ts
  • src/pages/api/close/*.ts
  • src/pages/api/update/*.ts