Skip to main content

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.ts middleware calls schema.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 FileCorresponding Type FileSchema Count
user.validators.tsuser.types.ts2
workspace.validators.tsworkspace.types.ts3
platform.validators.tsplatform.types.ts1
post.validators.tspost.types.ts3
schedule.validators.tsschedule.types.ts2
ai.validators.tsai.types.ts6
brand-voice.validators.tsbrand-voice.types.ts2
calendar.validators.tscalendar.types.ts2
repurpose.validators.tsrepurpose.types.ts1
trend.validators.tstrend.types.ts1
conversation.validators.tsconversation.types.ts3
ice.validators.tsice.types.ts4
analytics.validators.tsanalytics.types.ts2
ab-test.validators.tsab-test.types.ts2
prediction.validators.tsprediction.types.ts1
benchmark.validators.tsbenchmark.types.ts1
ecommerce.validators.tsecommerce.types.ts2
commerce.validators.tscommerce.types.ts1
ads.validators.tsads.types.ts2
workflow.validators.tsworkflow.types.ts2
audience.validators.tsaudience.types.ts2
competitor.validators.tscompetitor.types.ts2
approval.validators.tsapproval.types.ts1
report.validators.tsreport.types.ts1
notification.validators.tsnotification.types.ts1
integration.validators.tsintegration.types.ts2
intelligence.validators.tsintelligence.types.ts1
plan.validators.tsplan.types.ts1
admin.validators.tsadmin.types.ts1

Benefits of Shared Validation​

BenefitDescription
Single source of truthSame validation rules enforce on both client and server
Type inferencez.infer<typeof schema> generates TypeScript types automatically
Consistent error messagesSame validation messages on frontend forms and API responses
Compile-time safetySchema changes break dependent code at compile time
DRYNo duplicated validation logic between frontend and backend

Cross-Reference