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