Validating external data from APIs, JSON parsing, user input, and any untrusted sources in TypeScript applications
Validates all external data from APIs, files, databases, and user input using Zod schemas. Triggers when receiving data from any untrusted source to ensure runtime type safety matches TypeScript types.
/plugin marketplace add djankies/claude-configs/plugin install typescript@claude-configsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
Purpose: Ensure runtime type safety by validating all data from external sources. TypeScript types are compile-time only and erased at runtime.
When to use: Any time you receive data from outside your TypeScript code - API responses, JSON files, user input, database queries, environment variables, file uploads, or any external system.
This code has NO runtime safety:
interface User {
id: string;
email: string;
age: number;
}
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
Why it's unsafe:
response.json() returns any (or unknown with strict settings)as User doesn't validate data{ id: 123, email: null, age: "twenty" }, TypeScript accepts itTypeScript types are documentation, not validation.
Safe code validates at system boundaries:
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number().int().positive(),
});
type User = z.infer<typeof UserSchema>;
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return UserSchema.parse(data);
}
Now:
External data enters at these boundaries:
process.envValidate at every boundary. Never trust external data.
Advantages:
Alternative libraries:
io-ts - More functional, steeper learning curveyup - Popular but not TypeScript-nativejoi - Older, less TypeScript-friendlyajv - JSON Schema validator, verboseZod is the best choice for TypeScript projects.
Primitives:
import { z } from 'zod';
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const nullSchema = z.null();
const undefinedSchema = z.undefined();
Objects:
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().max(120),
verified: z.boolean(),
createdAt: z.string().datetime(),
});
type User = z.infer<typeof UserSchema>;
Arrays:
const UsersSchema = z.array(UserSchema);
type Users = z.infer<typeof UsersSchema>;
Optional fields:
const UserSchema = z.object({
id: z.string(),
name: z.string(),
nickname: z.string().optional(),
age: z.number().nullable(),
});
Default values:
const ConfigSchema = z.object({
port: z.number().default(3000),
host: z.string().default('localhost'),
});
const PostSchema = z.object({
id: z.number(),
title: z.string(),
body: z.string(),
userId: z.number(),
});
type Post = z.infer<typeof PostSchema>;
async function getPost(id: number): Promise<Post> {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return PostSchema.parse(data);
}
What this does:
async function getPost(id: number): Promise<Post | null> {
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
if (!response.ok) {
console.error(`HTTP error! status: ${response.status}`);
return null;
}
const data = await response.json();
return PostSchema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
console.error('Validation failed:', error.errors);
error.errors.forEach((err) => {
console.error(`${err.path.join('.')}: ${err.message}`);
});
} else {
console.error('Unexpected error:', error);
}
return null;
}
}
ZodError provides detailed information:
error.errors - Array of validation errorserr.path - Field path that failederr.message - Human-readable error messageerr.code - Error type codeasync function getPost(id: number): Promise<Post | null> {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${id}`
);
const data = await response.json();
const result = PostSchema.safeParse(data);
if (result.success) {
return result.data;
} else {
console.error('Validation failed:', result.error.errors);
return null;
}
}
safeParse returns:
{ success: true, data: T } on success{ success: false, error: ZodError } on failureUse when:
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/),
country: z.string().length(2),
});
const UserSchema = z.object({
id: z.string(),
name: z.string(),
address: AddressSchema,
});
const SuccessResponseSchema = z.object({
status: z.literal('success'),
data: z.object({
id: z.string(),
name: z.string(),
}),
});
const ErrorResponseSchema = z.object({
status: z.literal('error'),
message: z.string(),
code: z.number(),
});
const APIResponseSchema = z.discriminatedUnion('status', [
SuccessResponseSchema,
ErrorResponseSchema,
]);
type APIResponse = z.infer<typeof APIResponseSchema>;
function handleResponse(response: APIResponse) {
if (response.status === 'success') {
console.log(response.data.name);
} else {
console.error(response.message, response.code);
}
}
type Category = {
id: string;
name: string;
subcategories: Category[];
};
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
id: z.string(),
name: z.string(),
subcategories: z.array(CategorySchema),
})
);
Transform (convert data):
const TimestampSchema = z.string().transform((str) => new Date(str));
Refine (custom validation):
const PasswordSchema = z
.string()
.min(8)
.refine(
(password) => /[A-Z]/.test(password),
{ message: 'Must contain uppercase letter' }
)
.refine(
(password) => /[0-9]/.test(password),
{ message: 'Must contain number' }
);
import { readFile } from 'fs/promises';
import { z } from 'zod';
const ConfigSchema = z.object({
database: z.object({
host: z.string(),
port: z.number(),
username: z.string(),
password: z.string(),
}),
server: z.object({
port: z.number().default(3000),
host: z.string().default('localhost'),
}),
});
type Config = z.infer<typeof ConfigSchema>;
async function loadConfig(path: string): Promise<Config> {
const content = await readFile(path, 'utf-8');
const data = JSON.parse(content);
return ConfigSchema.parse(data);
}
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
type Env = z.infer<typeof EnvSchema>;
function validateEnv(): Env {
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment variables:');
console.error(result.error.errors);
process.exit(1);
}
return result.data;
}
export const env = validateEnv();
Usage:
import { env } from './env';
const server = app.listen(env.PORT, () => {
console.log(`Server running on port ${env.PORT}`);
});
Benefits:
import { Pool } from 'pg';
const pool = new Pool();
const UserRowSchema = z.object({
id: z.string(),
email: z.string(),
created_at: z.string(),
});
type UserRow = z.infer<typeof UserRowSchema>;
async function getUser(id: string): Promise<UserRow> {
const result = await pool.query(
'SELECT id, email, created_at FROM users WHERE id = $1',
[id]
);
if (result.rows.length === 0) {
throw new Error('User not found');
}
return UserRowSchema.parse(result.rows[0]);
}
Database Safety Note: Use parameterized queries (with $1, $2 placeholders) to prevent SQL injection. For detailed patterns on safe query construction with Prisma, use the preventing-sql-injection skill from prisma-6 for database-specific sanitization patterns.
import { MongoClient } from 'mongodb';
const UserDocumentSchema = z.object({
_id: z.string(),
email: z.string(),
name: z.string(),
});
async function getUser(id: string) {
const client = await MongoClient.connect('mongodb://localhost');
const db = client.db('myapp');
const doc = await db.collection('users').findOne({ _id: id });
if (!doc) {
throw new Error('User not found');
}
return UserDocumentSchema.parse(doc);
}
import { WebSocket } from 'ws';
const MessageSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('chat'),
message: z.string(),
userId: z.string(),
}),
z.object({
type: z.literal('typing'),
userId: z.string(),
}),
]);
type Message = z.infer<typeof MessageSchema>;
const ws = new WebSocket('ws://localhost:8080');
ws.on('message', (data) => {
try {
const parsed = JSON.parse(data.toString());
const message = MessageSchema.parse(parsed);
if (message.type === 'chat') {
console.log(`${message.userId}: ${message.message}`);
} else {
console.log(`${message.userId} is typing...`);
}
} catch (error) {
console.error('Invalid message received:', error);
}
});
import express from 'express';
import { z } from 'zod';
const CreateUserSchema = z.object({
username: z.string().min(3).max(20),
email: z.string().email(),
password: z.string().min(12),
});
app.post('/register', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({
error: 'Validation failed',
details: result.error.errors,
});
return;
}
const user = await createUser(result.data);
res.json({ user });
});
import { z } from 'zod';
import { useState } from 'react';
const LoginSchema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
function LoginForm() {
const [errors, setErrors] = useState<Record<string, string>>({});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = {
email: formData.get('email'),
password: formData.get('password'),
};
const result = LoginSchema.safeParse(data);
if (!result.success) {
const fieldErrors: Record<string, string> = {};
result.error.errors.forEach((err) => {
const field = err.path[0] as string;
fieldErrors[field] = err.message;
});
setErrors(fieldErrors);
return;
}
await login(result.data);
};
return (
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
{errors.email && <span>{errors.email}</span>}
<input name="password" type="password" />
{errors.password && <span>{errors.password}</span>}
<button type="submit">Login</button>
</form>
);
}
DON'T recreate schemas on every call:
function validateUser(data: unknown) {
const schema = z.object({ id: z.string() });
return schema.parse(data);
}
DO create schemas once:
const UserSchema = z.object({ id: z.string() });
function validateUser(data: unknown) {
return UserSchema.parse(data);
}
Validate only needed fields:
const FullUserSchema = z.object({
id: z.string(),
email: z.string(),
name: z.string(),
address: z.object({
street: z.string(),
city: z.string(),
}),
});
const UserIdSchema = FullUserSchema.pick({ id: true });
const user = (await response.json()) as User;
This provides ZERO runtime safety.
const data = await response.json();
console.log(data.user.email);
UserSchema.parse(data);
Validation must come BEFORE use.
try {
return UserSchema.parse(data);
} catch {
return {};
}
Silently failing validation defeats its purpose.
z.infer)safeParse for non-throwing validationZod v4 Features:
Prisma 6 Database Validation:
Key Principles:
Pattern:
const Schema = z.object({});
type Type = z.infer<typeof Schema>;
function processExternalData(data: unknown): Type {
return Schema.parse(data);
}
This ensures your TypeScript types match runtime reality, preventing the #1 cause of production bugs in TypeScript applications.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.