一键导入
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 职业分类
Guide 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.
| 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 handling