Shared Validators
The @unipulse/shared package contains 29 Zod schema files that validate data on both the frontend (form validation with React Hook Form) and the backend (request validation via validate.ts middleware).
Package Location
Pulse/packages/shared/src/validators/
How Validation Works
Single Source of Truth
The same Zod schema validates data on both sides:
- Backend: The
validate.tsmiddleware callsschema.parse(req.body)before the request reaches the route handler. - Frontend: React Hook Form uses
zodResolver(schema)to validate form inputs before submission.
This guarantees that if a form passes frontend validation, it will also pass backend validation (and vice versa).
Backend Usage
// apps/api/src/routes/post.routes.ts
import { createPostSchema, updatePostSchema } from '@unipulse/shared';
import { validate } from '../middleware/validate';
router.post('/posts',
authenticate,
requireWorkspace('EDITOR'),
validate(createPostSchema), // Validates req.body
postController.create
);
router.patch('/posts/:id',
authenticate,
requireWorkspace('EDITOR'),
validate(updatePostSchema),
postController.update
);
Validation Error Response (400)
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": [
{ "field": "caption", "message": "Required" },
{ "field": "platforms", "message": "Array must contain at least 1 element(s)" }
]
}
}
Frontend Usage
// apps/web/src/pages/composer/ComposerForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { createPostSchema, type CreatePostInput } from '@unipulse/shared';
function ComposerForm() {
const form = useForm<CreatePostInput>({
resolver: zodResolver(createPostSchema),
defaultValues: {
caption: '',
platforms: [],
mediaIds: [],
},
});
const onSubmit = (data: CreatePostInput) => {
// data is fully validated and typed
createPostMutation.mutate(data);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields with automatic validation */}
</form>
</Form>
);
}
Example Validator: Post
// packages/shared/src/validators/post.validators.ts
import { z } from 'zod';
export const createPostSchema = z.object({
caption: z.string().min(1, 'Caption is required').max(5000),
platforms: z.array(z.enum(['FACEBOOK', 'INSTAGRAM', 'TIKTOK'])).min(1, 'Select at least one platform'),
mediaIds: z.array(z.string()).optional(),
scheduledAt: z.string().datetime().optional(),
brandVoiceId: z.string().optional(),
});
export const updatePostSchema = createPostSchema.partial();
export type CreatePostInput = z.infer<typeof createPostSchema>;
export type UpdatePostInput = z.infer<typeof updatePostSchema>;
Example Validator: AI
// packages/shared/src/validators/ai.validators.ts
import { z } from 'zod';
export const generateCaptionSchema = z.object({
topic: z.string().min(1).max(500),
platform: z.enum(['FACEBOOK', 'INSTAGRAM', 'TIKTOK']),
tone: z.string().optional(),
brandVoiceId: z.string().optional(),
language: z.string().default('en'),
count: z.number().int().min(1).max(5).default(3),
});
export const rewriteCaptionSchema = z.object({
caption: z.string().min(1),
instruction: z.string().min(1),
platform: z.enum(['FACEBOOK', 'INSTAGRAM', 'TIKTOK']).optional(),
});
export const generateHashtagsSchema = z.object({
caption: z.string().min(1),
platform: z.enum(['FACEBOOK', 'INSTAGRAM', 'TIKTOK']),
count: z.number().int().min(1).max(30).default(10),
});
export type GenerateCaptionInput = z.infer<typeof generateCaptionSchema>;
export type RewriteCaptionInput = z.infer<typeof rewriteCaptionSchema>;
export type GenerateHashtagsInput = z.infer<typeof generateHashtagsSchema>;
Example Validator: Workflow
// packages/shared/src/validators/workflow.validators.ts
import { z } from 'zod';
const conditionOperator = z.enum(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'contains', 'not_contains']);
const actionType = z.enum([
'send_notification', 'create_draft', 'publish_post',
'send_reply', 'wait', 'boost_post', 'ai_repurpose_content',
]);
export const createWorkflowSchema = z.object({
name: z.string().min(1).max(255),
description: z.string().max(1000).optional(),
nodes: z.array(z.object({
id: z.string(),
type: z.enum(['trigger', 'condition', 'action']),
data: z.record(z.unknown()),
position: z.object({ x: z.number(), y: z.number() }),
})),
edges: z.array(z.object({
id: z.string(),
source: z.string(),
target: z.string(),
})),
});
export type CreateWorkflowInput = z.infer<typeof createWorkflowSchema>;
Complete Validator File List
| Validator File | Corresponding Type File | Schema Count |
|---|---|---|
user.validators.ts | user.types.ts | 2 |
workspace.validators.ts | workspace.types.ts | 3 |
platform.validators.ts | platform.types.ts | 1 |
post.validators.ts | post.types.ts | 3 |
schedule.validators.ts | schedule.types.ts | 2 |
ai.validators.ts | ai.types.ts | 6 |
brand-voice.validators.ts | brand-voice.types.ts | 2 |
calendar.validators.ts | calendar.types.ts | 2 |
repurpose.validators.ts | repurpose.types.ts | 1 |
trend.validators.ts | trend.types.ts | 1 |
conversation.validators.ts | conversation.types.ts | 3 |
ice.validators.ts | ice.types.ts | 4 |
analytics.validators.ts | analytics.types.ts | 2 |
ab-test.validators.ts | ab-test.types.ts | 2 |
prediction.validators.ts | prediction.types.ts | 1 |
benchmark.validators.ts | benchmark.types.ts | 1 |
ecommerce.validators.ts | ecommerce.types.ts | 2 |
commerce.validators.ts | commerce.types.ts | 1 |
ads.validators.ts | ads.types.ts | 2 |
workflow.validators.ts | workflow.types.ts | 2 |
audience.validators.ts | audience.types.ts | 2 |
competitor.validators.ts | competitor.types.ts | 2 |
approval.validators.ts | approval.types.ts | 1 |
report.validators.ts | report.types.ts | 1 |
notification.validators.ts | notification.types.ts | 1 |
integration.validators.ts | integration.types.ts | 2 |
intelligence.validators.ts | intelligence.types.ts | 1 |
plan.validators.ts | plan.types.ts | 1 |
admin.validators.ts | admin.types.ts | 1 |
Benefits of Shared Validation
| Benefit | Description |
|---|---|
| Single source of truth | Same validation rules enforce on both client and server |
| Type inference | z.infer<typeof schema> generates TypeScript types automatically |
| Consistent error messages | Same validation messages on frontend forms and API responses |
| Compile-time safety | Schema changes break dependent code at compile time |
| DRY | No duplicated validation logic between frontend and backend |
Cross-Reference
- Types -- corresponding type definitions
- Adding New Types -- how to add new validators
- Middleware -- validate.ts middleware
- Components -- form components using zodResolver