Here's a critical vulnerability in the "entity submission endpoint" of a real nuxt project that demonstrates why TypeScript alone isn't enough - you need runtime validation with Zod.
Here's what's happening in your /api/entity/submit.post.ts
endpoint:
export interface BaseEntity {
id: number
name: string
slug: string
module: string
categories?: Category[]
topics?: Topic[]
// ... other fields
}
export const submitForm = userSchema.table('submit_form', {
formData: jsonb('form_data').notNull(),
identifier: varchar('identifier', { length: 255 }).notNull(),
module: varchar('module', { length: 255 }).notNull(),
// ... other fields
})
const data = await readBody(event) // ❌ No validation!
// Manual validation scattered throughout
if (data.categories) {
if (!Array.isArray(data.categories)) {
return { status: 400, message: 'Categories must be an array' }
}
}
// Direct database insertion with unvalidated data
await getDb()
.insert(submitForm)
.values({
formData: JSON.stringify(data), // ❌ Whatever the user sends!
identifier: data.slug || data.domain,
// ...
})
// Frontend has Zod validation ✅
const schema = z.object({
name: z.string().min(1, "Name is required"),
domain: z.string().regex(/^[\w.-]+\.[a-z]{2,}$/i, "Enter a valid domain"),
pricing: z.array(z.string()).min(1, "Pick at least one pricing option"),
// ...
})
// But API can be called directly, bypassing frontend validation!
- SQL Injection via JSONB - Malformed JSON could break queries
- XSS Attacks - Unvalidated HTML/scripts stored in database
- Data Corruption - Invalid data types crash the application
- Domain Spoofing - No validation on domain format
- Array Injection - Categories/topics could contain invalid IDs
Create a shared validation schema:
import { z } from 'zod'
export const entitySubmitSchema = z.object({
name: z.string().min(1).max(255),
domain: z.string()
.regex(/^[\w.-]+\.[a-z]{2,}$/i)
.transform(val => val.toLowerCase()),
slug: z.string().optional(),
pricing: z.array(z.enum(['Free', 'Paid', 'Subscription', 'Free Trial'])),
tags: z.union([
z.string().transform(val => val.split(',').map(t => t.trim())),
z.array(z.string())
]),
oneLiner: z.string().min(1).max(75),
description: z.string().min(1).max(5000),
categories: z.array(z.number().int().positive()),
topics: z.array(z.number().int().positive()).optional(),
logo: z.string().url().optional().nullable(),
uuid: z.string().uuid(),
})
export type EntitySubmitInput = z.infer<typeof entitySubmitSchema>
// packages/api/server/api/entity/submit.post.ts
import { entitySubmitSchema } from '../../../../db/validations/entity'
import { validateBody } from '../../../../utils/server/utils/bodyValidation'
export default defineEventHandler(async (event) => {
try {
const session = await requireUserSession(event)
if (!session?.user?.id) {
throw createError({ statusCode: 401, statusMessage: 'Unauthorized' })
}
// ✅ Validate and transform data
const validatedData = await validateBody(event, entitySubmitSchema)
// ✅ Type-safe from here on
const { categories, topics, domain, tags } = validatedData
// Now TypeScript knows exact types AND runtime validates them!
} catch (error) {
if (error instanceof z.ZodError) {
throw createError({
statusCode: 400,
statusMessage: `Validation error: ${error.errors[0].message}`
})
}
throw error
}
})
Without Zod, a malicious user could:
# Send invalid data directly to your API
curl -X POST https://yoursite.com/api/entity/submit \
-H "Content-Type: application/json" \
-d '{
"name": "<script>alert(\"XSS\")</script>",
"domain": "not-a-domain",
"categories": "not-an-array",
"tags": {"malicious": "object"},
"description": null
}'
With Zod, this request would be rejected with clear error messages before touching your database.
- Runtime Safety - Catches errors TypeScript can't
- Data Transformation - Normalize domains, parse tags
- Better Error Messages - Users get helpful validation feedback
- Single Source of Truth - Share schemas between frontend/backend
- Type Inference - Generate TypeScript types from schemas
- Security - Prevent injection attacks and data corruption
Your codebase already uses Zod in auth endpoints - extending this pattern to all user input endpoints would significantly improve security and reliability.