"매 type schema = single source of truth". Zod는 매 TypeScript type을 런타임 schema 로 정의하고, parse 시점에 매 validation + type narrowing 을 동시 제공한다. API boundary, env vars, form input 매 unsafe input 의 매 첫 검증선.
매 핵심
매 동기 (Why Zod over alternatives)
TypeScript types are erased: 매 컴파일 후 interface User는 매 사라짐 → API response 의 data as User 매 lying.
Zod = schema → type: z.infer<typeof Schema> 로 매 schema 가 source of truth.
Composability: 매 .merge, .partial, .extend, .transform 으로 매 schema 합성.
Error-rich: parse failure 시 매 path, code, message tree 반환.
매 경쟁 라이브러리
Yup: 매 older, schema → type 약함, 매 Zod 가 대체.
io-ts: 매 더 functional (fp-ts), 매 learning curve 높음.
Valibot: 매 tree-shakable, 매 bundle size 우선이면 고려 (~10x smaller).
ArkType: 매 string-based syntax, 매 빠르지만 ecosystem 작음.
Zod: 매 default choice in 2026 — DX, ecosystem (tRPC, React Hook Form), maturity.
매 응용
API boundary: fetch response 매 parse 후 typed 으로 사용.
Form validation: React Hook Form + zodResolver.
env vars: process.env 의 매 schema parse, missing key 시 즉시 fail.
DB row → domain: ORM 결과 매 Schema.parse(row).
LLM structured output: Claude/GPT JSON response 매 schema 로 검증.
💻 패턴
Pattern 1: 기본 schema + type inference
import{z}from"zod";exportconstUserSchema=z.object({id: z.string().uuid(),email: z.string().email(),age: z.number().int().min(0).max(150),role: z.enum(["admin","user","guest"]),createdAt: z.coerce.date(),});exporttypeUser=z.infer<typeofUserSchema>;// usage
constraw: unknown=awaitfetchUser();constuser=UserSchema.parse(raw);// throws ZodError if invalid
// ^? User
constEnvSchema=z.object({DATABASE_URL: z.string().url(),PORT: z.coerce.number().int().positive().default(3000),NODE_ENV: z.enum(["development","production","test"]),ANTHROPIC_API_KEY: z.string().startsWith("sk-ant-"),});exportconstenv=EnvSchema.parse(process.env);// process exits at startup if invalid — better than runtime surprise
Pattern 4: discriminated union
constResultSchema=z.discriminatedUnion("status",[z.object({status: z.literal("ok"),data: z.string()}),z.object({status: z.literal("error"),code: z.number()}),]);typeResult=z.infer<typeofResultSchema>;// narrowing on .status works correctly
Pattern 5: transform for parse-not-validate
constDateSchema=z.string().transform((s,ctx)=>{constd=newDate(s);if(isNaN(d.getTime())){ctx.addIssue({code:"custom",message:"Invalid date"});returnz.NEVER;}returnd;});constout=DateSchema.parse("2026-05-10");// Date instance
Pattern 6: API client with Zod
asyncfunctionfetchUser(id: string):Promise<User>{constres=awaitfetch(`/api/users/${id}`);constjson=awaitres.json();returnUserSchema.parse(json);// unknown → User
}
constPasswordSchema=z.object({password: z.string(),confirm: z.string()}).refine((d)=>d.password===d.confirm,{message:"Passwords do not match",path:["confirm"],});
매 결정 기준
상황
Approach
API boundary, untrusted input
Zod parse
Internal pure-TS code
Type only, no Zod
Bundle size critical (mobile, edge)
Valibot
Functional ergonomics
io-ts
LLM structured output (Claude/GPT)
Zod + tool schema
Performance hot path (>10k parses/sec)
Compile to TypeBox/AJV
기본값: Zod 3.x — 매 modern TS app 의 default validation layer.
언제: untrusted boundary (API, form, env, LLM output) 매 parse. tool/function calling 의 매 input schema.
언제 X: internal pure-TS code 매 over-validation 불필요. hot loop 의 매 매 parse 호출.
❌ 안티패턴
Anti1: parse everywhere: 매 internal function 매 Zod parse — 매 overhead 누적, 매 type only 충분.
Anti2: as cast after parse: Schema.parse(x) as MyType — 매 redundant, parse 가 이미 typed return.
Anti3: schema duplication: type + schema 따로 정의 — 매 z.infer 사용.
Anti4: nested transforms with side effects: transform 안에서 fetch/IO — 매 pure 하게 유지.