원클릭으로
data-validation-patterns
Guide for validating external data with Zod. Use when fetching from APIs, processing webhooks, or handling user input.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Guide for validating external data with Zod. Use when fetching from APIs, processing webhooks, or handling user input.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | data-validation-patterns |
| description | Guide for validating external data with Zod. Use when fetching from APIs, processing webhooks, or handling user input. |
Ensure all external data is validated using Zod schemas before use. Prevents runtime type errors and provides safe fallbacks.
ALL data from external sources MUST be validated:
All validation schemas live in lib/validation/:
lib/validation/
├── event.ts # Event DTOs, paged responses
├── city.ts # City/region DTOs
├── place.ts # Place DTOs
└── category.ts # Category DTOs
Rule: Create schemas alongside the API client that uses them.
// lib/validation/my-resource.ts
import { z } from "zod";
export const MyResourceSchema = z.object({
id: z.string(),
name: z.string(),
count: z.number(),
optional: z.string().optional(),
nullable: z.string().nullable(),
});
export type MyResource = z.infer<typeof MyResourceSchema>;
// For arrays
export const MyResourceArraySchema = z.array(MyResourceSchema);
// For paged responses
export const PagedMyResourceSchema = z.object({
content: z.array(MyResourceSchema),
currentPage: z.number(),
pageSize: z.number(),
totalElements: z.number(),
totalPages: z.number(),
last: z.boolean(),
});
// lib/validation/my-resource.ts
export function parseMyResource(data: unknown): MyResource | null {
const result = MyResourceSchema.safeParse(data);
if (!result.success) {
console.error("MyResource validation failed:", result.error.format());
return null;
}
return result.data;
}
export function parseMyResourceArray(data: unknown): MyResource[] {
const result = MyResourceArraySchema.safeParse(data);
if (!result.success) {
console.error("MyResource array validation failed:", result.error.format());
return []; // Safe fallback
}
return result.data;
}
// lib/api/my-resource-external.ts
// NOTE: fetchWithHmac is ONLY used in external wrappers (Layer 3)
// See api-layer-patterns skill for the three-layer architecture
import { captureException } from "@sentry/nextjs";
import { fetchWithHmac } from "@lib/api/fetch-wrapper";
import { parseMyResource } from "@lib/validation/my-resource";
const API_URL = process.env.NEXT_PUBLIC_API_URL;
export async function fetchMyResourceExternal(id: string): Promise<MyResource | null> {
if (!API_URL) return null; // Environment guard
try {
const response = await fetchWithHmac(`${API_URL}/api/my-resource/${id}`);
const data = await response.json();
const validated = parseMyResource(data);
if (!validated) {
captureException(new Error("MyResource validation failed"), {
tags: { section: "my-resource", type: "validation-failed" },
extra: { id, data },
});
return null;
}
return validated;
} catch (error) {
captureException(error, {
tags: { section: "my-resource", type: "fetch-failed" },
extra: { id },
});
return null;
}
}
Reference these existing patterns:
| Parser | Location | Returns |
|---|---|---|
parsePagedEvents | lib/validation/event.ts | Paged events or null |
parseEventDetail | lib/validation/event.ts | Event detail or null |
parseCategorizedEvents | lib/validation/event.ts | Categorized events or null |
PlaceResponseArraySchema | lib/validation/place.ts | Place array |
CitySummaryArraySchema | lib/validation/city.ts | City array |
Always capture validation failures to Sentry:
import { captureException } from "@sentry/nextjs";
export function parseWithSentry<T>(
schema: z.ZodSchema<T>,
data: unknown,
context: { section: string; extra?: Record<string, unknown> }
): T | null {
const result = schema.safeParse(data);
if (!result.success) {
console.error(
`${context.section} validation failed:`,
result.error.format()
);
// Create error with validation details for better Sentry context
const validationError = new Error(
`${context.section} validation failed: ${result.error.message}`
);
validationError.cause = result.error; // Preserve original Zod error
captureException(validationError, {
tags: {
section: context.section,
type: "validation-failed",
},
extra: {
...context.extra,
zodErrors: result.error.format(),
},
});
return null;
}
return result.data;
}
nullexport function parseEvent(data: unknown): EventDetail | null {
const result = EventDetailSchema.safeParse(data);
return result.success ? result.data : null;
}
// Usage
const event = parseEvent(data);
if (!event) {
return notFound(); // or handle gracefully
}
[]export function parseEvents(data: unknown): Event[] {
const result = EventArraySchema.safeParse(data);
return result.success ? result.data : [];
}
// Usage - always safe to map
const events = parseEvents(data);
return events.map((e) => <EventCard key={e.id} event={e} />);
const emptyPage: PagedResponse<Event> = {
content: [],
currentPage: 0,
pageSize: 10,
totalElements: 0,
totalPages: 0,
last: true,
};
export function parsePagedEvents(data: unknown): PagedResponse<Event> {
const result = PagedEventSchema.safeParse(data);
return result.success ? result.data : emptyPage;
}
z.string().optional(); // string | undefined
z.string().nullable(); // string | null
z.string().nullish(); // string | null | undefined
z.number().default(0);
z.string().default("");
z.array(z.string()).default([]);
// Coerce string to number
z.coerce.number();
// Transform after validation
z.string().transform((s) => s.toLowerCase());
// Date from string
z.string()
.datetime()
.transform((s) => new Date(s));
const ResponseSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("success"), data: DataSchema }),
z.object({ type: z.literal("error"), message: z.string() }),
]);
From lib/stripe/webhook.ts:
const StripeWebhookEventSchema = z.object({
id: z.string().min(1),
type: z.string().min(1),
data: z.object({
object: z.record(z.unknown()),
}),
});
export type StripeWebhookEvent = z.infer<typeof StripeWebhookEventSchema>;
export function validateWebhookEvent(
payload: unknown
): StripeWebhookEvent | null {
const result = StripeWebhookEventSchema.safeParse(payload);
if (!result.success) {
console.error("Webhook validation failed:", result.error.format());
return null;
}
return result.data;
}
lib/validation/?safeParse (not parse) to avoid throws?z.infer<typeof Schema>?parse instead of safeParse → Throws on invalid dataundefined instead of null → Inconsistent handlingGuide for adding environment variables. Use when adding new env vars to ensure all 5 locations are updated (code, env files, Coolify, workflow, GitHub secrets).
Enforce the internal API proxy layer pattern. Use when adding API endpoints, fetching data from backend, implementing HMAC signing, or working with lib/api/* or app/api/*.
Best practices for React 19 and Next.js 16 App Router. Use when creating components, hooks, or pages. Enforces server-first rendering, proper client boundaries, and modern React patterns.
MANDATORY checklist before writing ANY new code. Use this skill FIRST before implementing any feature, component, utility, type, or hook. Enforces DRY principle and existing pattern reuse.
Evaluate external PR suggestions (Gemini, CodeRabbit) against project conventions. Use when asked to review or agree with external code review comments.
Guide for CSP, security headers, and external scripts. Use fetchWithHmac for APIs, safeFetch for external services.