Validation and Response Helpers

Validation + Consistent Responses: Making Your API Feel “Professional”

Welcome back! In this lesson, we’ll take your Task Manager API from “it works” to “it’s predictable and safe to use.” That means validating everything that comes in (so bad input can’t sneak through) and standardizing everything that goes out (so every client knows exactly what shape to expect).

You’ll do this in a very “Codex CLI” way: small, targeted prompts that touch a couple of files at a time, with clear rules about behavior and response shape. By the end, your /api/tasks, /api/tasks/[id], and /api/tasks/filter endpoints will all speak the same JSON “language,” and they’ll reject invalid requests with helpful, consistent error payloads.

Previously…

In the previous lesson, you focused on polishing the API surface: introducing response helpers and refactoring routes to use them so every response had a consistent envelope. Now we’ll build on that foundation by adding Zod-powered request validation (instead of ad-hoc checks), and we’ll apply it across create/update flows so the backend is just as strict and predictable as the response format.

How we’ll use Codex CLI in this lesson

The key habit is to treat Codex like a teammate who needs constraints. For each change, you’ll give a prompt that:

  • lists the exact files Codex may modify,
  • describes the exact response shape and status codes,
  • and explicitly says “don’t touch anything else.”

For example:

Codex, modify only src/lib/validation/taskSchemas.ts.

Implement Zod schemas for task create/update: createTaskSchema, putTaskSchema, and patchTaskSchema.

Keep the field rules: title/content required non-empty strings, completed default false, dueDate optional but must parse as a date string.

Do not modify any other files. Show the full updated file.

That “only these files” pattern is what keeps vibe coding from turning into mystery refactors.

Defining task rules once with Zod schemas

Validation is easiest to maintain when you define it one time, then reuse it everywhere. In this project, that “single source of truth” for task input validation lives in src/lib/validation/taskSchemas.ts.

// src/lib/validation/taskSchemas.ts
import { z } from 'zod';

export const createTaskSchema = z.object({
  title: z.string().min(1, "'title' must be a non-empty string."),
  content: z.string().min(1, "'content' must be a non-empty string."),
  completed: z.boolean().default(false),
  dueDate: z.string().optional().refine(
    (val) => val === undefined || !Number.isNaN(Date.parse(val)),
    { message: "'dueDate' must be a valid date string." }
  ),
});

// PUT: All fields required
export const putTaskSchema = createTaskSchema;

// PATCH: All fields optional
export const patchTaskSchema = createTaskSchema.partial();
  • createTaskSchema defines the exact shape your API accepts when creating tasks: required title/content, a boolean completed (defaulting to false), and an optional dueDate that must be parseable as a date string. This keeps your API strict without needing repetitive “if/else validation” in every route.

  • .refine(...) is doing the “date sanity check.” Instead of trusting that a string looks like a date, it ensures Date.parse(...) doesn’t fail—so "not-a-date" can’t slip in and break later logic.

  • putTaskSchema and patchTaskSchema share the same base rules, but express different update semantics: PUT requires the full object, while PATCH makes every field optional via .partial(). This mirrors common REST expectations and keeps your update logic consistent.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal