Input-Only Validators
What We Did
Section titled “What We Did”Refactored @repo/validators to only contain input schemas for form validation, moving backend-specific schemas (ID operations, pagination) into the Convex backend. This creates a clear separation between user-facing form validation and backend API schemas.
Why This Approach
Section titled “Why This Approach”Key reasons:
- Single responsibility: Validators package should only handle user input validation
- Clear boundaries: Backend schemas (get by ID, delete, list pagination) are implementation details
- Form focus: Input schemas represent data users submit, not API operations
- Simpler frontend: Forms only need the core input schema, not operation-specific variants
Alternatives considered:
- Keep all schemas in validators: Creates confusion about which schemas are for forms vs API
- Duplicate schemas: Would lead to drift and maintenance burden
Implementation Details
Section titled “Implementation Details”Before (validators/things.ts)
Section titled “Before (validators/things.ts)”The package exported multiple schema types:
// Form input schemasexport const createThingSchema = z.object({...})export const updateThingSchema = z.object({ id, ...fields })
// Backend operation schemas (not for forms)export const getThingSchema = z.object({ id })export const removeThingSchema = z.object({ id })export const listThingsSchema = z.object({ limit })After (validators/things.ts)
Section titled “After (validators/things.ts)”A single, focused input schema:
/** * Core input schema for Thing entity * Used for: Create forms (all fields as-is), Update forms (use .partial() in backend) */export const thingInputSchema = z.object({ title: z.string().min(1, "Title is required").max(200, "Title must be 200 characters or less"), description: z.string().max(2000, "Description must be 2000 characters or less").optional(), imageId: z.string().optional(),})
export type ThingInput = z.infer<typeof thingInputSchema>Backend (convex/functions/things.ts)
Section titled “Backend (convex/functions/things.ts)”Backend now defines its own operation schemas:
import { thingInputSchema } from "@repo/validators/things"
// Backend-specific schemasconst idSchema = z.object({ id: z.string() })const listSchema = z.object({ limit: z.number().int().min(1).max(100).optional() })
// Update derives from input schema with nullable supportconst updateSchema = idSchema.extend({ title: thingInputSchema.shape.title.optional(), description: z.string().max(2000).nullable().optional(), imageId: z.string().nullable().optional(),})
// Create uses the shared input schema directlyexport const create = authMutation.input(thingInputSchema).mutation(...)
// Get/Remove use the local ID schemaexport const get = authQuery.input(idSchema).query(...)export const remove = authMutation.input(idSchema).mutation(...)
// List uses the local list schemaexport const list = authQuery.input(listSchema).query(...)Frontend (page.tsx)
Section titled “Frontend (page.tsx)”Forms use the single input schema:
import { thingInputSchema } from "@repo/validators/things"
// Validation uses schema shapevalidators={{ onChange: ({ value }) => { const result = thingInputSchema.shape.title.safeParse(value) return result.success ? undefined : result.error.issues[0]?.message },}}Key Benefits
Section titled “Key Benefits”- Validators package is simpler - Only exports what forms need
- Backend owns its API - Operation schemas are co-located with handlers
- Type safety preserved - Backend derives update schema from input schema
- No duplication - Core validation rules defined once
Context for AI
Section titled “Context for AI”When working with validators and schemas:
@repo/validatorscontains ONLY input schemas for user data- Backend operation schemas (id-based, pagination) live in the backend
- Use
thingInputSchema.shape.fieldNamefor field-level validation in forms - Backend can derive update schemas:
inputSchema.partial().extend({ id }) - Nullable fields (for clearing) are backend concerns, not form concerns
Testing/Verification
Section titled “Testing/Verification”# Run validator teststurbo test --filter=@repo/validators
# Run backend teststurbo test --filter=backend
# Run web teststurbo test --filter=web
# Type check affected packagesturbo check-types --filter=@repo/validators --filter=backend --filter=webExpected results:
- 19 validator tests pass (focused on thingInputSchema)
- 40 backend tests pass (unchanged behavior)
- 6 web tests pass
- All type checks pass
Related Documentation
Section titled “Related Documentation”- Shared Validators Package - Original validators setup
- Better Convex Folder Structure - Backend organization