Untyped JSON is where production bugs hide. Zod gives you runtime validation and TypeScript types from a single schema, perfect for Route Handlers and Server Actions.
What you will build
- A reusable `parseJson` helper
- A Zod schema for a job application or contact payload
- A POST route that returns field-level errors on invalid input
Prerequisites
- Next.js App Router project
- `npm install zod`
- Familiarity with REST status codes (400, 422, 500)
Step-by-step
- Create `lib/schemas/job-application.ts` with a Zod object schema for name, email, phone, and coverLetter.
- Export the inferred type: `export type JobApplicationInput = z.infer<typeof jobApplicationSchema>`.
- Add `lib/http/parse-json.ts` that reads `request.json()`, runs `schema.safeParse`, and returns `{ data }` or `{ error }`.
- In `app/api/careers/apply/route.ts`, call the parser; on failure return `NextResponse.json({ fields: ... }, { status: 422 })`.
- On success, persist to database or file and return `{ ok: true }`.
- Add unit tests for the schema with valid and invalid payloads.
Schema definition
import { z } from "zod";
export const jobApplicationSchema = z.object({
name: z.string().trim().min(2, "Name is required."),
email: z.string().trim().email("Enter a valid email."),
phone: z.string().trim().min(8, "Enter a valid phone number."),
coverLetter: z.string().trim().min(40, "Cover letter is too short."),
jobSlug: z.string().trim().min(1),
});
export type JobApplicationInput = z.infer<typeof jobApplicationSchema>;Route Handler
import { NextResponse } from "next/server";
import { jobApplicationSchema } from "@/lib/schemas/job-application";
export async function POST(request: Request) {
const json = await request.json().catch(() => null);
const parsed = jobApplicationSchema.safeParse(json);
if (!parsed.success) {
return NextResponse.json(
{ error: "Validation failed.", fields: parsed.error.flatten().fieldErrors },
{ status: 422 },
);
}
// save parsed.data
return NextResponse.json({ ok: true });
}Common mistakes
- Validating only on the client, always re-validate on the server
- Using `.parse()` in routes, prefer `safeParse` for friendly API errors
- Forgetting to trim strings before min-length checks
Next steps
Share schemas between client forms (React Hook Form + `@hookform/resolvers/zod`) and server routes so validation rules never drift.


