Initial commit
This commit is contained in:
175
skills/using-runtime-checks/references/error-handling.md
Normal file
175
skills/using-runtime-checks/references/error-handling.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Validation Error Handling
|
||||
|
||||
## Basic Error Handling
|
||||
|
||||
### Using safeParse
|
||||
|
||||
```typescript
|
||||
const result = UserSchema.safeParse(data);
|
||||
|
||||
if (result.success) {
|
||||
const user: User = result.data;
|
||||
console.log(user.name);
|
||||
} else {
|
||||
console.error("Validation failed:", result.error);
|
||||
}
|
||||
```
|
||||
|
||||
### Using parse (throwing)
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const user = UserSchema.parse(data);
|
||||
console.log(user.name);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
console.error("Validation failed:", error.issues);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Processing Validation Issues
|
||||
|
||||
### Extracting Error Details
|
||||
|
||||
```typescript
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues.map(issue => ({
|
||||
path: issue.path.join("."),
|
||||
message: issue.message,
|
||||
code: issue.code
|
||||
}));
|
||||
|
||||
throw new ValidationError("Invalid user data", issues);
|
||||
}
|
||||
```
|
||||
|
||||
### User-Friendly Error Messages
|
||||
|
||||
```typescript
|
||||
function formatValidationError(error: z.ZodError): string {
|
||||
return error.issues
|
||||
.map(issue => {
|
||||
const field = issue.path.join(".");
|
||||
return `${field}: ${issue.message}`;
|
||||
})
|
||||
.join(", ");
|
||||
}
|
||||
|
||||
try {
|
||||
const validated = validateLoginForm(form);
|
||||
await login(validated);
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
const message = formatValidationError(error);
|
||||
showError(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Recovery Strategies
|
||||
|
||||
### Fallback Values
|
||||
|
||||
```typescript
|
||||
const result = ConfigSchema.safeParse(data);
|
||||
|
||||
const config = result.success
|
||||
? result.data
|
||||
: getDefaultConfig();
|
||||
```
|
||||
|
||||
### Partial Success
|
||||
|
||||
```typescript
|
||||
function validateItemsWithPartialSuccess<T>(
|
||||
items: unknown[],
|
||||
schema: z.ZodType<T>
|
||||
): { valid: T[], invalid: { item: unknown, error: z.ZodError }[] } {
|
||||
const valid: T[] = [];
|
||||
const invalid: { item: unknown, error: z.ZodError }[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = schema.safeParse(item);
|
||||
if (result.success) {
|
||||
valid.push(result.data);
|
||||
} else {
|
||||
invalid.push({ item, error: result.error });
|
||||
}
|
||||
}
|
||||
|
||||
return { valid, invalid };
|
||||
}
|
||||
```
|
||||
|
||||
## Custom Error Classes
|
||||
|
||||
```typescript
|
||||
class ValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly issues: Array<{ path: string; message: string }>
|
||||
) {
|
||||
super(message);
|
||||
this.name = "ValidationError";
|
||||
}
|
||||
}
|
||||
|
||||
function validateOrThrow<T>(data: unknown, schema: z.ZodType<T>): T {
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues.map(issue => ({
|
||||
path: issue.path.join("."),
|
||||
message: issue.message
|
||||
}));
|
||||
throw new ValidationError("Validation failed", issues);
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
## Logging Validation Errors
|
||||
|
||||
```typescript
|
||||
function validateWithLogging<T>(
|
||||
data: unknown,
|
||||
schema: z.ZodType<T>,
|
||||
context: string
|
||||
): T | null {
|
||||
const result = schema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
logger.error({
|
||||
context,
|
||||
error: "Validation failed",
|
||||
issues: result.error.issues,
|
||||
data: sanitizeForLogging(data)
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
```
|
||||
|
||||
## API Error Responses
|
||||
|
||||
```typescript
|
||||
app.post("/api/users", (req, res) => {
|
||||
const result = UserSchema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
error: "Invalid request body",
|
||||
details: result.error.issues.map(issue => ({
|
||||
field: issue.path.join("."),
|
||||
message: issue.message
|
||||
}))
|
||||
});
|
||||
}
|
||||
|
||||
const user = result.data;
|
||||
});
|
||||
```
|
||||
211
skills/using-runtime-checks/references/performance.md
Normal file
211
skills/using-runtime-checks/references/performance.md
Normal file
@@ -0,0 +1,211 @@
|
||||
# Validation Performance Optimization
|
||||
|
||||
## Schema Reuse
|
||||
|
||||
**BAD: Creating schemas repeatedly**
|
||||
```typescript
|
||||
function validateUser(data: unknown) {
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
});
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
**GOOD: Reusing schemas**
|
||||
```typescript
|
||||
const UserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string()
|
||||
});
|
||||
|
||||
function validateUser(data: unknown) {
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Boundary Validation
|
||||
|
||||
Validate once at system boundaries, not internally:
|
||||
|
||||
```typescript
|
||||
async function fetchUser(id: string): Promise<User> {
|
||||
const response = await fetch(`/api/users/${id}`);
|
||||
const data: unknown = await response.json();
|
||||
|
||||
return UserSchema.parse(data);
|
||||
}
|
||||
|
||||
function processUser(user: User) {
|
||||
const fullName = formatName(user);
|
||||
return fullName;
|
||||
}
|
||||
```
|
||||
|
||||
## Lazy Schema Definition
|
||||
|
||||
For schemas with circular dependencies:
|
||||
|
||||
```typescript
|
||||
const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
parent: CategorySchema.optional()
|
||||
}));
|
||||
```
|
||||
|
||||
## Selective Validation
|
||||
|
||||
Validate only what's needed:
|
||||
|
||||
```typescript
|
||||
const FullUserSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
profile: z.object({
|
||||
bio: z.string(),
|
||||
avatar: z.string().url()
|
||||
}),
|
||||
settings: z.object({
|
||||
theme: z.enum(["light", "dark"]),
|
||||
notifications: z.boolean()
|
||||
})
|
||||
});
|
||||
|
||||
const BasicUserSchema = FullUserSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
email: true
|
||||
});
|
||||
```
|
||||
|
||||
## Batch Validation
|
||||
|
||||
For arrays, validate the array once rather than each item:
|
||||
|
||||
```typescript
|
||||
const UsersSchema = z.array(UserSchema);
|
||||
|
||||
async function fetchAllUsers(): Promise<User[]> {
|
||||
const response = await fetch("/api/users");
|
||||
const data: unknown = await response.json();
|
||||
|
||||
return UsersSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Early Validation
|
||||
|
||||
Fail fast on critical fields:
|
||||
|
||||
```typescript
|
||||
const CriticalFirstSchema = z.object({
|
||||
apiKey: z.string().uuid(),
|
||||
timestamp: z.number()
|
||||
}).strict();
|
||||
|
||||
const FullRequestSchema = CriticalFirstSchema.extend({
|
||||
payload: z.unknown()
|
||||
});
|
||||
```
|
||||
|
||||
## Caching Parsed Results
|
||||
|
||||
```typescript
|
||||
const schemaCache = new WeakMap<object, User>();
|
||||
|
||||
function getCachedUser(data: unknown): User {
|
||||
if (typeof data === "object" && data !== null) {
|
||||
const cached = schemaCache.get(data);
|
||||
if (cached) return cached;
|
||||
}
|
||||
|
||||
const user = UserSchema.parse(data);
|
||||
|
||||
if (typeof data === "object" && data !== null) {
|
||||
schemaCache.set(data, user);
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
```
|
||||
|
||||
## Avoiding Redundant Validation
|
||||
|
||||
```typescript
|
||||
class UserService {
|
||||
private validated = new WeakSet<object>();
|
||||
|
||||
async processUser(data: unknown): Promise<void> {
|
||||
if (typeof data !== "object" || data === null) {
|
||||
throw new Error("Invalid data");
|
||||
}
|
||||
|
||||
if (!this.validated.has(data)) {
|
||||
UserSchema.parse(data);
|
||||
this.validated.add(data);
|
||||
}
|
||||
|
||||
const user = data as User;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
```typescript
|
||||
async function validateWithMetrics<T>(
|
||||
data: unknown,
|
||||
schema: z.ZodType<T>,
|
||||
name: string
|
||||
): Promise<T> {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
const result = schema.parse(data);
|
||||
const duration = performance.now() - start;
|
||||
|
||||
metrics.record(`validation.${name}.success`, duration);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = performance.now() - start;
|
||||
metrics.record(`validation.${name}.failure`, duration);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Alternative Libraries for Performance
|
||||
|
||||
For maximum performance in hot paths:
|
||||
|
||||
**TypeBox** (compile-time optimized):
|
||||
```typescript
|
||||
import { Type, Static } from "@sinclair/typebox";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
|
||||
const UserType = Type.Object({
|
||||
id: Type.String(),
|
||||
name: Type.String(),
|
||||
email: Type.String({ format: "email" })
|
||||
});
|
||||
|
||||
type User = Static<typeof UserType>;
|
||||
|
||||
const user = Value.Parse(UserType, data);
|
||||
```
|
||||
|
||||
**Valibot** (smaller bundle size):
|
||||
```typescript
|
||||
import * as v from "valibot";
|
||||
|
||||
const UserSchema = v.object({
|
||||
id: v.string(),
|
||||
name: v.string(),
|
||||
email: v.pipe(v.string(), v.email())
|
||||
});
|
||||
|
||||
type User = v.InferOutput<typeof UserSchema>;
|
||||
```
|
||||
241
skills/using-runtime-checks/references/zod-patterns.md
Normal file
241
skills/using-runtime-checks/references/zod-patterns.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# Zod Patterns Reference
|
||||
|
||||
## Advanced Schema Composition
|
||||
|
||||
### Union Types
|
||||
|
||||
```typescript
|
||||
const SuccessResponseSchema = z.object({
|
||||
status: z.literal("success"),
|
||||
data: z.unknown()
|
||||
});
|
||||
|
||||
const ErrorResponseSchema = z.object({
|
||||
status: z.literal("error"),
|
||||
error: z.string(),
|
||||
code: z.number()
|
||||
});
|
||||
|
||||
const ApiResponseSchema = z.discriminatedUnion("status", [
|
||||
SuccessResponseSchema,
|
||||
ErrorResponseSchema
|
||||
]);
|
||||
|
||||
type ApiResponse = z.infer<typeof ApiResponseSchema>;
|
||||
```
|
||||
|
||||
### Transform and Coerce
|
||||
|
||||
```typescript
|
||||
const DateSchema = z.string().datetime().transform(str => new Date(str));
|
||||
|
||||
const UserWithDatesSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
createdAt: DateSchema,
|
||||
updatedAt: DateSchema
|
||||
});
|
||||
|
||||
type UserWithDates = z.infer<typeof UserWithDatesSchema>;
|
||||
|
||||
const CoerceNumberSchema = z.coerce.number();
|
||||
|
||||
const QueryParamsSchema = z.object({
|
||||
page: CoerceNumberSchema.int().positive().default(1),
|
||||
limit: CoerceNumberSchema.int().min(1).max(100).default(20),
|
||||
sort: z.enum(["asc", "desc"]).default("asc")
|
||||
});
|
||||
```
|
||||
|
||||
### Custom Refinements
|
||||
|
||||
```typescript
|
||||
const PasswordSchema = z
|
||||
.string()
|
||||
.min(8)
|
||||
.refine(
|
||||
password => /[A-Z]/.test(password),
|
||||
"Password must contain at least one uppercase letter"
|
||||
)
|
||||
.refine(
|
||||
password => /[a-z]/.test(password),
|
||||
"Password must contain at least one lowercase letter"
|
||||
)
|
||||
.refine(
|
||||
password => /[0-9]/.test(password),
|
||||
"Password must contain at least one number"
|
||||
);
|
||||
|
||||
const SignupSchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: PasswordSchema,
|
||||
confirmPassword: z.string()
|
||||
}).refine(
|
||||
data => data.password === data.confirmPassword,
|
||||
{
|
||||
message: "Passwords don't match",
|
||||
path: ["confirmPassword"]
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
### Partial and Optional Schemas
|
||||
|
||||
```typescript
|
||||
const UpdateUserSchema = UserSchema.partial();
|
||||
|
||||
type UpdateUser = z.infer<typeof UpdateUserSchema>;
|
||||
|
||||
async function updateUser(id: string, updates: unknown): Promise<User> {
|
||||
const validated = UpdateUserSchema.parse(updates);
|
||||
|
||||
const response = await fetch(`/api/users/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify(validated)
|
||||
});
|
||||
|
||||
return UserSchema.parse(await response.json());
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Objects
|
||||
|
||||
```typescript
|
||||
const AddressSchema = z.object({
|
||||
street: z.string(),
|
||||
city: z.string(),
|
||||
state: z.string().length(2),
|
||||
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
|
||||
});
|
||||
|
||||
const UserWithAddressSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
address: AddressSchema,
|
||||
billingAddress: AddressSchema.optional()
|
||||
});
|
||||
|
||||
type UserWithAddress = z.infer<typeof UserWithAddressSchema>;
|
||||
```
|
||||
|
||||
### Array Validation
|
||||
|
||||
```typescript
|
||||
const TagSchema = z.string().min(1).max(20);
|
||||
|
||||
const PostSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string().min(1).max(200),
|
||||
content: z.string(),
|
||||
tags: z.array(TagSchema).min(1).max(10),
|
||||
metadata: z.record(z.string(), z.unknown())
|
||||
});
|
||||
|
||||
type Post = z.infer<typeof PostSchema>;
|
||||
|
||||
async function fetchPosts(): Promise<Post[]> {
|
||||
const response = await fetch("/api/posts");
|
||||
const data: unknown = await response.json();
|
||||
|
||||
const PostsSchema = z.array(PostSchema);
|
||||
return PostsSchema.parse(data);
|
||||
}
|
||||
```
|
||||
|
||||
## Generic Validation Helpers
|
||||
|
||||
```typescript
|
||||
async function apiCall<T>(
|
||||
endpoint: string,
|
||||
dataSchema: z.ZodType<T>
|
||||
): Promise<T> {
|
||||
const response = await fetch(endpoint);
|
||||
const rawData: unknown = await response.json();
|
||||
|
||||
const apiResponse = ApiResponseSchema.parse(rawData);
|
||||
|
||||
if (apiResponse.status === "error") {
|
||||
throw new Error(`API Error ${apiResponse.code}: ${apiResponse.error}`);
|
||||
}
|
||||
|
||||
return dataSchema.parse(apiResponse.data);
|
||||
}
|
||||
```
|
||||
|
||||
## Validation Middleware
|
||||
|
||||
```typescript
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
|
||||
function validateBody<T>(schema: z.ZodType<T>) {
|
||||
return (req: Request, res: Response, next: NextFunction) => {
|
||||
const result = schema.safeParse(req.body);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json({
|
||||
error: "Validation failed",
|
||||
issues: result.error.issues
|
||||
});
|
||||
}
|
||||
|
||||
req.body = result.data;
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
app.post("/users", validateBody(UserSchema), (req, res) => {
|
||||
const user: User = req.body;
|
||||
});
|
||||
```
|
||||
|
||||
## Safe JSON Parse
|
||||
|
||||
```typescript
|
||||
function safeJsonParse<T>(
|
||||
json: string,
|
||||
schema: z.ZodType<T>
|
||||
): T {
|
||||
try {
|
||||
const data: unknown = JSON.parse(json);
|
||||
return schema.parse(data);
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) {
|
||||
throw new Error("Invalid JSON");
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration Validation
|
||||
|
||||
```typescript
|
||||
const ConfigSchema = z.object({
|
||||
port: z.number().int().positive().default(3000),
|
||||
database: z.object({
|
||||
host: z.string(),
|
||||
port: z.number().int().positive(),
|
||||
name: z.string()
|
||||
}),
|
||||
redis: z.object({
|
||||
url: z.string().url()
|
||||
}).optional()
|
||||
});
|
||||
|
||||
type Config = z.infer<typeof ConfigSchema>;
|
||||
|
||||
function loadConfig(): Config {
|
||||
const data: unknown = process.env;
|
||||
|
||||
return ConfigSchema.parse({
|
||||
port: data.PORT,
|
||||
database: {
|
||||
host: data.DB_HOST,
|
||||
port: data.DB_PORT,
|
||||
name: data.DB_NAME
|
||||
},
|
||||
redis: data.REDIS_URL ? { url: data.REDIS_URL } : undefined
|
||||
});
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user