TypeScriptAPI design
Zod at the API boundary: validate, then trust your types
Runtime validation for untrusted JSON, consistent 400 responses, and schemas that can align with OpenAPI and handler logic.
Untrusted input—mobile clients, reverse proxies, partner integrations—must be validated before it touches domain logic. Asserting req.body as CreateDto documents intent but guarantees nothing at runtime. Zod (or equivalent) gives parse-or-reject semantics with structured errors.
Schema-first handlers
import { z } from "zod";
export const CreateProjectBodySchema = z.object({
name: z.string().min(1).max(120),
repoUrl: z.string().url(),
visibility: z.enum(["private", "internal", "public"]),
});
export type CreateProjectBody = z.infer<typeof CreateProjectBodySchema>;
export function parseCreateProject(raw: unknown) {
const r = CreateProjectBodySchema.safeParse(raw);
if (!r.success) {
return {
ok: false as const,
status: 400,
body: { error: "validation_error", issues: r.error.flatten() },
};
}
return { ok: true as const, value: r.data };
}
HTTP mapping
export async function postProject(req: Request) {
const raw = await req.json().catch(() => undefined);
const parsed = parseCreateProject(raw);
if (!parsed.ok) {
return Response.json(parsed.body, { status: parsed.status });
}
const project = await createProject(parsed.value);
return Response.json(project, { status: 201 });
}
Documentation alignment
The same schema can feed OpenAPI generation or contract tests, narrowing the gap between published docs and runtime behavior.
Takeaway: Validation at the boundary keeps domain types honest and turns malformed input into a controlled 400, not a 500 stack trace.