Zod Complete Guide | TypeScript Schemas
이 글의 핵심
Zod bridges TypeScript types and runtime data. Define a schema once, infer static types with z.infer, and validate unknown JSON, env vars, and form input — without maintaining parallel validation logic.
What This Guide Covers
You will learn how to:
- Model data with primitives, objects, unions, and enums
- Add refinements and transforms (e.g., string → number)
- Use
safeParsevsparseat boundaries - Validate HTTP bodies and environment variables
- Pair Zod with React Hook Form
1. Basics: parse vs safeParse
import { z } from 'zod';
const emailSchema = z.string().email();
// Throws ZodError on failure
emailSchema.parse('hi@example.com');
// Returns { success, data } | { success, error }
const result = emailSchema.safeParse('not-an-email');
if (!result.success) {
console.error(result.error.flatten());
} else {
console.log(result.data);
}
2. Objects & Type Inference
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
age: z.number().int().nonnegative().optional(),
role: z.enum(['user', 'admin']),
});
type User = z.infer<typeof UserSchema>;
Nested objects and arrays compose naturally:
const AddressSchema = z.object({
line1: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5}$/),
});
const OrderSchema = z.object({
id: z.string(),
items: z.array(z.object({ sku: z.string(), qty: z.number().int().positive() })).min(1),
shipTo: AddressSchema,
});
3. Unions, Discriminated Unions, and Literals
const EventSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('key'), key: z.string() }),
]);
4. Transforms & Coercion
const QuerySchema = z.object({
page: z.coerce.number().int().min(1).default(1),
q: z.string().trim().optional(),
});
// "3" from query strings becomes number 3
QuerySchema.parse({ page: '3', q: ' hello ' });
Use .transform for custom mapping:
const IsoDateSchema = z.string().transform((s) => new Date(s));
5. Refinements (Cross-Field Rules)
const PasswordSchema = z
.object({
password: z.string().min(8),
confirm: z.string(),
})
.refine((data) => data.password === data.confirm, {
message: 'Passwords must match',
path: ['confirm'],
});
6. API Request Validation (Express-Style)
import type { Request, Response } from 'express';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
export async function createUser(req: Request, res: Response) {
const parsed = CreateUserSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
issues: parsed.error.issues,
});
}
const { email, name } = parsed.data;
// ... persist
return res.status(201).json({ ok: true });
}
7. Environment Variables
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'test', 'production']),
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().default(3000),
});
export const env = EnvSchema.parse(process.env);
Fail fast at startup instead of deep in the request path.
8. React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const FormSchema = z.object({
title: z.string().min(3),
accept: z.literal(true, {
errorMap: () => ({ message: 'You must accept the terms' }),
}),
});
type FormValues = z.infer<typeof FormSchema>;
export function PostForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormValues>({
resolver: zodResolver(FormSchema),
});
return (
<form onSubmit={handleSubmit((data) => console.log(data))}>
<input {...register('title')} />
{errors.title && <p>{errors.title.message}</p>}
<label>
<input type="checkbox" {...register('accept')} /> I agree
</label>
{errors.accept && <p>{errors.accept.message}</p>}
<button type="submit">Save</button>
</form>
);
}
9. Error Shapes for Clients
const result = UserSchema.safeParse(payload);
if (!result.success) {
const flat = result.error.flatten();
// flat.fieldErrors, flat.formErrors
}
For APIs, return error.issues (or a flattened map) so clients can show field-level messages.
10. Best Practices
Do
- Validate at every trust boundary (HTTP, queue messages, CLI args).
- Keep schemas next to the API contract or feature module.
- Prefer
safeParseat IO edges; reserveparsefor tests and internal helpers.
Avoid
- Reusing huge generic schemas for every endpoint — split by use case.
- Validating giant blobs synchronously on every tick — batch or debounce when needed.
Summary & Checklist
- Schema = single source of truth for runtime + types (
z.infer). safeParsefor user-facing failures;parsewhen failure should be fatal.- Transforms and refinements encode business rules declaratively.
- React Hook Form + zodResolver keeps UI errors aligned with server rules.
Checklist
- Replace ad-hoc
ifchecks with Zod on critical paths - Add
EnvSchema.parseat startup - Return structured
issuesfrom APIs on 400 - Share form schemas between client and server when possible (monorepo)
More career guides (Korean on pkglog.com)
Deep dives in Korean: tech interview prep, resume & interviews, practical job search.
Related posts:
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Master Zod for TypeScript: primitives, objects, refinements, transforms, safe parsing, React Hook Form integration, API … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- TypeScript 5 Complete Guide | Decorators· satisfies
- Next.js 15 Complete Guide | Turbopack· React 19
- React Hook Form Complete Guide | Form Management· Validation
이 글에서 다루는 키워드 (관련 검색어)
Zod, TypeScript, Validation, Schema, React Hook Form, API 등으로 검색하시면 이 글이 도움이 됩니다.