Initial commit
This commit is contained in:
433
skills/validating-query-inputs/SKILL.md
Normal file
433
skills/validating-query-inputs/SKILL.md
Normal file
@@ -0,0 +1,433 @@
|
||||
---
|
||||
name: validating-query-inputs
|
||||
description: Validate all external input with Zod before Prisma operations. Use when accepting user input, API requests, or form data.
|
||||
allowed-tools: Read, Write, Edit
|
||||
---
|
||||
|
||||
# Input Validation with Zod and Prisma 6
|
||||
|
||||
## Overview
|
||||
|
||||
Always validate external input with Zod before Prisma operations. Never trust user-provided data, API requests, or form submissions. Use type-safe validation pipelines that match Prisma schema types.
|
||||
|
||||
## Validation Pipeline
|
||||
|
||||
```
|
||||
External Input → Zod Validation → Transform → Prisma Operation
|
||||
```
|
||||
|
||||
### Pattern
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1).max(100),
|
||||
age: z.number().int().positive().optional()
|
||||
})
|
||||
|
||||
async function createUser(rawInput: unknown) {
|
||||
const validatedData = createUserSchema.parse(rawInput)
|
||||
|
||||
return await prisma.user.create({
|
||||
data: validatedData
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## Zod Schemas for Prisma Models
|
||||
|
||||
### Matching Prisma Types
|
||||
|
||||
```prisma
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
name String
|
||||
phone String?
|
||||
website String?
|
||||
age Int?
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
|
||||
const phoneRegex = /^\+?[1-9]\d{1,14}$/
|
||||
|
||||
const userCreateSchema = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
name: z.string().min(1).max(100).trim(),
|
||||
phone: z.string().regex(phoneRegex).optional(),
|
||||
website: z.string().url().optional(),
|
||||
age: z.number().int().min(0).max(150).optional()
|
||||
})
|
||||
|
||||
const userUpdateSchema = userCreateSchema.partial()
|
||||
|
||||
type UserCreateInput = z.infer<typeof userCreateSchema>
|
||||
type UserUpdateInput = z.infer<typeof userUpdateSchema>
|
||||
```
|
||||
|
||||
### Common Validation Patterns
|
||||
|
||||
```typescript
|
||||
const emailSchema = z.string().email().toLowerCase().trim()
|
||||
|
||||
const urlSchema = z.string().url().refine(
|
||||
(url) => url.startsWith('https://'),
|
||||
{ message: 'URL must use HTTPS' }
|
||||
)
|
||||
|
||||
const phoneSchema = z.string().regex(
|
||||
/^\+?[1-9]\d{1,14}$/,
|
||||
'Invalid phone number format'
|
||||
)
|
||||
|
||||
const slugSchema = z.string()
|
||||
.min(1)
|
||||
.max(100)
|
||||
.regex(/^[a-z0-9-]+$/, 'Slug must contain only lowercase letters, numbers, and hyphens')
|
||||
|
||||
const dateSchema = z.coerce.date().refine(
|
||||
(date) => date > new Date(),
|
||||
{ message: 'Date must be in the future' }
|
||||
)
|
||||
```
|
||||
|
||||
## Complete Validation Examples
|
||||
|
||||
### API Route with Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const createPostSchema = z.object({
|
||||
title: z.string().min(1).max(200),
|
||||
content: z.string().min(1),
|
||||
authorId: z.string().cuid(),
|
||||
published: z.boolean().default(false),
|
||||
tags: z.array(z.string()).max(10).optional()
|
||||
})
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const rawBody = await request.json()
|
||||
const validatedData = createPostSchema.parse(rawBody)
|
||||
|
||||
const post = await prisma.post.create({
|
||||
data: validatedData
|
||||
})
|
||||
|
||||
return Response.json(post)
|
||||
} catch (error) {
|
||||
if (error instanceof z.ZodError) {
|
||||
return Response.json(
|
||||
{ errors: error.errors },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Form Data Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const profileUpdateSchema = z.object({
|
||||
name: z.string().min(1).max(100).trim(),
|
||||
bio: z.string().max(500).trim().optional(),
|
||||
website: z.string().url().optional().or(z.literal('')),
|
||||
location: z.string().max(100).trim().optional(),
|
||||
birthDate: z.coerce.date().max(new Date()).optional()
|
||||
})
|
||||
|
||||
async function updateProfile(userId: string, formData: FormData) {
|
||||
const rawData = {
|
||||
name: formData.get('name'),
|
||||
bio: formData.get('bio'),
|
||||
website: formData.get('website'),
|
||||
location: formData.get('location'),
|
||||
birthDate: formData.get('birthDate')
|
||||
}
|
||||
|
||||
const validatedData = profileUpdateSchema.parse(rawData)
|
||||
|
||||
return await prisma.user.update({
|
||||
where: { id: userId },
|
||||
data: validatedData
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Nested Object Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const addressSchema = z.object({
|
||||
street: z.string().min(1).max(200),
|
||||
city: z.string().min(1).max(100),
|
||||
state: z.string().length(2).toUpperCase(),
|
||||
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/)
|
||||
})
|
||||
|
||||
const createCompanySchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
email: z.string().email().toLowerCase(),
|
||||
website: z.string().url().optional(),
|
||||
address: addressSchema
|
||||
})
|
||||
|
||||
async function createCompany(rawInput: unknown) {
|
||||
const validatedData = createCompanySchema.parse(rawInput)
|
||||
|
||||
return await prisma.company.create({
|
||||
data: {
|
||||
name: validatedData.name,
|
||||
email: validatedData.email,
|
||||
website: validatedData.website,
|
||||
address: {
|
||||
create: validatedData.address
|
||||
}
|
||||
},
|
||||
include: {
|
||||
address: true
|
||||
}
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Bulk Operation Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const bulkUserSchema = z.object({
|
||||
users: z.array(
|
||||
z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
name: z.string().min(1).max(100),
|
||||
role: z.enum(['USER', 'ADMIN'])
|
||||
})
|
||||
).min(1).max(100)
|
||||
})
|
||||
|
||||
async function createBulkUsers(rawInput: unknown) {
|
||||
const validatedData = bulkUserSchema.parse(rawInput)
|
||||
|
||||
const uniqueEmails = new Set(validatedData.users.map(u => u.email))
|
||||
if (uniqueEmails.size !== validatedData.users.length) {
|
||||
throw new Error('Duplicate emails in bulk operation')
|
||||
}
|
||||
|
||||
return await prisma.$transaction(
|
||||
validatedData.users.map(user =>
|
||||
prisma.user.create({ data: user })
|
||||
)
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Custom Refinements
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const passwordSchema = z.string()
|
||||
.min(8)
|
||||
.refine((pwd) => /[A-Z]/.test(pwd), {
|
||||
message: 'Password must contain uppercase letter'
|
||||
})
|
||||
.refine((pwd) => /[a-z]/.test(pwd), {
|
||||
message: 'Password must contain lowercase letter'
|
||||
})
|
||||
.refine((pwd) => /[0-9]/.test(pwd), {
|
||||
message: 'Password must contain number'
|
||||
})
|
||||
|
||||
const registerSchema = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
password: passwordSchema,
|
||||
confirmPassword: z.string()
|
||||
}).refine((data) => data.password === data.confirmPassword, {
|
||||
message: "Passwords don't match",
|
||||
path: ['confirmPassword']
|
||||
})
|
||||
```
|
||||
|
||||
### Async Validation
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const createUserSchema = z.object({
|
||||
email: z.string().email().toLowerCase(),
|
||||
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/)
|
||||
})
|
||||
|
||||
async function createUserWithChecks(rawInput: unknown) {
|
||||
const validatedData = createUserSchema.parse(rawInput)
|
||||
|
||||
const existing = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [
|
||||
{ email: validatedData.email },
|
||||
{ username: validatedData.username }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
if (existing) {
|
||||
if (existing.email === validatedData.email) {
|
||||
throw new Error('Email already exists')
|
||||
}
|
||||
if (existing.username === validatedData.username) {
|
||||
throw new Error('Username already taken')
|
||||
}
|
||||
}
|
||||
|
||||
return await prisma.user.create({
|
||||
data: validatedData
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Safe Parsing
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
|
||||
const updateSettingsSchema = z.object({
|
||||
theme: z.enum(['light', 'dark']).default('light'),
|
||||
notifications: z.boolean().default(true),
|
||||
language: z.string().length(2).default('en')
|
||||
})
|
||||
|
||||
async function updateSettings(userId: string, rawInput: unknown) {
|
||||
const result = updateSettingsSchema.safeParse(rawInput)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
success: false,
|
||||
errors: result.error.errors
|
||||
}
|
||||
}
|
||||
|
||||
const settings = await prisma.userSettings.upsert({
|
||||
where: { userId },
|
||||
update: result.data,
|
||||
create: {
|
||||
userId,
|
||||
...result.data
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: settings
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Checklist
|
||||
|
||||
- [ ] All external input validated before Prisma operations
|
||||
- [ ] Zod schemas match Prisma model types
|
||||
- [ ] Email addresses normalized (toLowerCase)
|
||||
- [ ] String inputs trimmed where appropriate
|
||||
- [ ] URLs validated and HTTPS enforced
|
||||
- [ ] Phone numbers validated with regex
|
||||
- [ ] Numeric ranges validated (min/max)
|
||||
- [ ] Array lengths limited (prevent DoS)
|
||||
- [ ] Unique constraints validated before bulk operations
|
||||
- [ ] Async existence checks for unique fields
|
||||
- [ ] Error messages don't leak sensitive data
|
||||
- [ ] File uploads validated (type, size, content)
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
### Never Trust Input
|
||||
|
||||
```typescript
|
||||
async function createUser(data: any) {
|
||||
return await prisma.user.create({ data })
|
||||
}
|
||||
```
|
||||
|
||||
### Never Skip Validation for "Internal" Data
|
||||
|
||||
```typescript
|
||||
async function createUserFromAdmin(data: unknown) {
|
||||
return await prisma.user.create({ data })
|
||||
}
|
||||
```
|
||||
|
||||
### Never Validate After Database Operation
|
||||
|
||||
```typescript
|
||||
async function createUser(data: unknown) {
|
||||
const user = await prisma.user.create({ data })
|
||||
const validated = schema.parse(user)
|
||||
return validated
|
||||
}
|
||||
```
|
||||
|
||||
## Type Safety Integration
|
||||
|
||||
```typescript
|
||||
import { z } from 'zod'
|
||||
import { Prisma } from '@prisma/client'
|
||||
|
||||
const userCreateSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string()
|
||||
}) satisfies z.Schema<Prisma.UserCreateInput>
|
||||
|
||||
type ValidatedUserInput = z.infer<typeof userCreateSchema>
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
**Zod v4 Validation:**
|
||||
|
||||
- If normalizing string inputs (trim, toLowerCase), use the transforming-string-methods skill for Zod v4 built-in string transformations
|
||||
- If using Zod for schema construction, use the validating-schema-basics skill from zod-4 for core validation patterns
|
||||
- If customizing validation error messages, use the customizing-errors skill from zod-4 for error formatting strategies
|
||||
- If validating string formats (email, UUID, URL), use the validating-string-formats skill from zod-4 for built-in validators
|
||||
|
||||
**TypeScript Validation:**
|
||||
|
||||
- If performing runtime type checking beyond Zod, use the using-runtime-checks skill from typescript for assertion patterns
|
||||
- If validating external data sources, use the validating-external-data skill from typescript for comprehensive validation strategies
|
||||
Reference in New Issue
Block a user