ufh-schema
v0.1.2
Published
Shared Zod domain schemas for United for Humanity (frontend + backend).
Maintainers
Readme
ufh-schema
Shared Zod domain schemas for United for Humanity (UFH) platform. Use this package in both frontend and backend to ensure type-safe API contracts and form validation.
Table of Contents
- Installation
- Quick Start
- Architecture
- Module Reference
- Frontend Usage
- Backend Usage
- TypeScript Types
- Best Practices
- Contributing
Installation
# npm
npm install ufh-schema zod
# pnpm
pnpm add ufh-schema zod
# yarn
yarn add ufh-schema zodNote:
zodis a peer dependency. This package supports Zod v3.25+ and v4.0+.
Quick Start
import { Auth, User, Survey, Donation, Admin, Shared } from 'ufh-schema';
// Import pre-exported types directly (RECOMMENDED)
import type { LoginInput, UserPublic, SurveyTemplate } from 'ufh-schema';
// Validate login input
const loginResult = Auth.LoginInputSchema.safeParse({
email: '[email protected]',
password: 'securePassword123',
});
if (loginResult.success) {
const data: LoginInput = loginResult.data; // Type is pre-exported!
console.log('Valid login data:', data);
} else {
console.error('Validation errors:', loginResult.error.issues);
}Architecture
ufh-schema
├── Auth # Authentication & session management
├── User # User profiles, settings, dashboards
├── Survey # Survey templates, questions, responses
├── Donation # Payment processing, history
├── Admin # Admin-only management schemas
├── Shared # Common utilities (pagination, enums, errors)
└── System # Health checks, error responsesDesign Principles
- Single Source of Truth: Define schemas once, use everywhere
- Pre-exported Types: Every schema exports its TypeScript type (e.g.,
LoginInputalongsideLoginInputSchema) - Namespace Exports: Import as
Auth.LoginInputSchemaorAuth.LoginInputfor clarity - Input/Output Separation:
*InputSchemafor requests,*Schemafor responses - Direct Type Imports:
import type { LoginInput } from 'ufh-schema'
Module Reference
Auth Module
Authentication, registration, and session management.
import { Auth } from 'ufh-schema';| Schema | Purpose | Example Usage |
|--------|---------|---------------|
| LoginInputSchema | User login | { phone, password } |
| RegisterBasicInputSchema | Initial registration | { firstName, lastName, phone, joinReason } |
| RegisterWithGoogleInputSchema | Google OAuth | { idToken, phone, joinReason } |
| ForgotPasswordInputSchema | Request password reset | { phone } |
| ResetPasswordInputSchema | Set new password | { token, newPassword } |
| VerifyEmailInputSchema | Verify email OTP | { email, otp } |
| SetEmailInputSchema | Set user email | { email } |
| SetPasswordInputSchema | Set initial password | { password } |
| RefreshTokenInputSchema | Refresh JWT | { refreshToken } |
| TokenPairSchema | Token response | { accessToken, refreshToken } |
| SessionSchema | Session data | { id, userId, jti, expiresAt, ... } |
Frontend Example: Login Form
import { Auth } from 'ufh-schema';
import type { LoginInput } from 'ufh-schema'; // Pre-exported type!
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
function LoginForm() {
const { register, handleSubmit, formState: { errors } } = useForm<LoginInput>({
resolver: zodResolver(Auth.LoginInputSchema),
});
const onSubmit = async (data: LoginInput) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(data),
});
// Handle response...
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('phone')} placeholder="+91 Phone Number" />
{errors.phone && <span>{errors.phone.message}</span>}
<input {...register('password')} type="password" placeholder="Password" />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}Backend Example: Login Endpoint (Express/Hono)
import { Auth } from 'ufh-schema';
import { Hono } from 'hono';
const app = new Hono();
app.post('/auth/login', async (c) => {
const body = await c.req.json();
// Validate request body
const result = Auth.LoginInputSchema.safeParse(body);
if (!result.success) {
return c.json({
success: false,
error: result.error.flatten()
}, 400);
}
const { phone, password } = result.data;
// Your authentication logic here...
const user = await authenticateUser(phone, password);
const tokens = await generateTokens(user);
return c.json({
success: true,
data: { user, tokens }
});
});User Module
User profiles, settings, and role-based dashboards.
import { User } from 'ufh-schema';| Schema | Purpose |
|--------|---------|
| UserPublicSchema | Public user profile |
| UserUpdateInputSchema | Self-service profile update |
| UserRoleUpdateInputSchema | Admin role change |
| UserSearchQuerySchema | User search/filter |
| UserSettingsSchema | User preferences |
| UserSettingsUpdateInputSchema | Update preferences |
| SupporterDashboardStatsSchema | Supporter dashboard |
| VolunteerDashboardStatsSchema | Volunteer dashboard |
| SurveyorDashboardStatsSchema | Surveyor dashboard |
| MentorDashboardStatsSchema | Mentor dashboard |
| ManagerDashboardStatsSchema | Manager dashboard |
Role Hierarchy
supporter → volunteer → surveyor → mentor → manager → superadminEach higher role inherits all schemas/stats of lower roles.
Dashboard Stats by Role
import { User, Shared } from 'ufh-schema';
// Get dashboard based on user role
function getDashboardSchema(role: Shared.Role) {
switch (role) {
case 'supporter':
return User.SupporterDashboardStatsSchema;
case 'volunteer':
return User.VolunteerDashboardStatsSchema;
case 'surveyor':
return User.SurveyorDashboardStatsSchema;
case 'mentor':
return User.MentorDashboardStatsSchema;
case 'manager':
case 'superadmin':
return User.ManagerDashboardStatsSchema;
}
}Survey Module
Multi-step survey forms with dynamic question types.
import { Survey } from 'ufh-schema';Template & Question Management
| Schema | Purpose |
|--------|---------|
| SurveyTemplateSchema | Survey template definition |
| SurveyTemplateCreateInputSchema | Create new template |
| SurveyTemplateUpdateInputSchema | Update template |
| SurveyTemplatePublishInputSchema | Publish draft template |
| SurveyQuestionSchema | Question definition |
| SurveyQuestionCreateInputSchema | Add question |
| SurveyQuestionUpdateInputSchema | Edit question |
| SurveyQuestionOptionSchema | Choice options |
| SurveyValidationRulesSchema | Field validation rules |
| SurveyStepSchema | Multi-step grouping |
| SurveyStepCreateInputSchema | Create step |
Survey Response Flow
| Schema | Purpose |
|--------|---------|
| SurveyInstanceSchema | Active survey session |
| SurveyInstanceCreateInputSchema | Start new survey |
| SurveyResponseStepInputSchema | Submit step answers |
| SurveyResponseSubmitInputSchema | Final submission |
| AvailableSurveyListItemSchema | Surveyor's survey list |
| FileUploadAnswerSchema | File/image uploads |
Custom Question Types (Admin)
| Schema | Purpose |
|--------|---------|
| CustomQuestionTypeSchema | Define new question type |
| CustomQuestionTypeCreateInputSchema | Create custom type |
| QuestionTypeConfigSchema | Type configuration |
Question Types Available
type QuestionType =
| 'single_choice' // Radio buttons
| 'multi_choice' // Checkboxes
| 'text' // Text input
| 'number' // Numeric input
| 'rating' // Star/scale rating
| 'date' // Date picker
| 'file_upload' // Document upload
| 'image_upload'; // Image uploadFrontend: Dynamic Survey Form
import { Survey, Shared } from 'ufh-schema';
import type { z } from 'zod';
type Question = z.infer<typeof Survey.SurveyQuestionSchema>;
function renderQuestion(question: Question) {
switch (question.type) {
case 'single_choice':
return (
<RadioGroup name={question.id}>
{question.options?.map(opt => (
<Radio key={opt.id} value={opt.id}>{opt.label}</Radio>
))}
</RadioGroup>
);
case 'multi_choice':
return (
<CheckboxGroup name={question.id}>
{question.options?.map(opt => (
<Checkbox key={opt.id} value={opt.id}>{opt.label}</Checkbox>
))}
</CheckboxGroup>
);
case 'text':
return <TextInput name={question.id} required={question.validation?.required} />;
case 'image_upload':
return <ImageUploader name={question.id} />;
// ... other types
}
}Backend: Create Survey Template
import { Survey } from 'ufh-schema';
async function createSurveyTemplate(req: Request) {
const result = Survey.SurveyTemplateCreateInputSchema.safeParse(req.body);
if (!result.success) {
throw new ValidationError(result.error);
}
const template = await db.surveyTemplates.create({
...result.data,
status: 'draft', // Always start as draft
version: 1,
createdBy: req.user.id,
});
return template;
}Donation Module
Payment processing with UPI intent support.
import { Donation } from 'ufh-schema';| Schema | Purpose |
|--------|---------|
| DonationCreateInputSchema | Initiate donation |
| DonationPublicSchema | Donation record |
| DonationConfirmInputSchema | Confirm payment |
| DonationHistoryQuerySchema | Filter donations |
| DonationListPayloadSchema | Paginated list |
Donation Methods
type DonationMethod =
| 'upi_intent' // UPI deeplink (current)
| 'upi_collect' // UPI collect request
| 'netbanking' // Net banking
| 'card' // Credit/Debit card
| 'wallet' // Digital wallets
| 'cash' // Offline cash
| 'bank_transfer'; // NEFT/RTGSDonation Purpose
type DonationPurpose =
| 'membership_fee' // Annual membership
| 'community_support' // Community fund
| 'general' // General donation
| 'campaign' // Specific campaign
| 'other';Frontend: Donation Form
import { Donation } from 'ufh-schema';
const DonationForm = () => {
const handleDonate = async (amount: number) => {
const result = Donation.DonationCreateInputSchema.safeParse({
amount,
method: 'upi_intent',
purpose: 'membership_fee',
isAnonymous: false,
});
if (!result.success) {
toast.error('Invalid donation data');
return;
}
const response = await api.post('/donations', result.data);
// Open UPI app via deeplink
window.location.href = response.data.upiDeeplink;
};
return (
<div>
<h2>Support Our Mission</h2>
<Button onClick={() => handleDonate(100)}>₹100</Button>
<Button onClick={() => handleDonate(500)}>₹500</Button>
<Button onClick={() => handleDonate(1000)}>₹1000</Button>
</div>
);
};Admin Module
Management schemas for superadmin role.
import { Admin } from 'ufh-schema';| Schema | Purpose |
|--------|---------|
| AdminDashboardStatsSchema | System-wide statistics |
| AdminStatsTimeSeriesSchema | Chart data |
| AdminDashboardQuerySchema | Date range filter |
| AdminUserSearchQuerySchema | Advanced user search |
| AdminBulkUserActionSchema | Bulk user operations |
| AdminSurveySearchQuerySchema | Survey search |
| SurveyCloneInputSchema | Clone template |
| BulkQuestionOrderUpdateSchema | Reorder questions |
| AdminDonationSearchQuerySchema | Donation search |
| DonationRefundInputSchema | Process refund |
| DonationReportQuerySchema | Generate reports |
| DonationPresetSchema | Suggested amounts |
Admin User Search (15+ Filters)
import { Admin } from 'ufh-schema';
// Example: Find volunteers who haven't donated, created this month
const query = Admin.AdminUserSearchQuerySchema.parse({
roles: ['volunteer', 'surveyor'],
hasCompletedDonation: false,
createdAfter: '2024-01-01',
sortBy: 'createdAt',
sortOrder: 'desc',
page: 1,
limit: 20,
});
const users = await api.get('/admin/users', { params: query });Bulk User Actions
import { Admin } from 'ufh-schema';
// Change role for multiple users
const action = Admin.AdminBulkUserActionSchema.parse({
userIds: ['user1', 'user2', 'user3'],
action: 'changeRole',
payload: { newRole: 'surveyor' },
});
await api.post('/admin/users/bulk', action);Shared Module
Common utilities used across all modules.
import { Shared } from 'ufh-schema';Enums & Primitives
| Schema | Values |
|--------|--------|
| RoleSchema | supporter, volunteer, surveyor, mentor, manager, superadmin |
| UserStatusSchema | pending, active, suspended, deleted |
| SurveyStatusSchema | draft, published, archived |
| DonationStatusSchema | initiated, pending, success, failed, cancelled, refunded |
| DonationMethodSchema | upi_intent, upi_collect, netbanking, card, wallet, cash, bank_transfer |
| DonationPurposeSchema | membership_fee, community_support, general, campaign, other |
| QuestionTypeSchema | single_choice, multi_choice, text, number, rating, date, file_upload, image_upload |
Validation Schemas
| Schema | Purpose |
|--------|---------|
| ObjectIdSchema | MongoDB ObjectId (24 hex chars) |
| EmailSchema | Email validation |
| PhoneSchema | Indian phone (+91) |
| IsoDateTimeStringSchema | ISO 8601 datetime |
Pagination
import { Shared } from 'ufh-schema';
// Standard pagination (page-based)
const query = Shared.PaginationQuerySchema.parse({
page: 1,
limit: 20,
search: 'query',
sortBy: 'createdAt',
sortOrder: 'desc',
});
// Cursor pagination (for infinite scroll)
const cursorQuery = Shared.CursorPaginationQuerySchema.parse({
cursor: 'abc123',
limit: 20,
direction: 'forward',
});API Response Helpers
import { Shared } from 'ufh-schema';
// Create standard success response schema
const UserResponseSchema = Shared.createApiSuccessResponseSchema(
z.object({ user: User.UserPublicSchema })
);
// Create response with error handling
const UserApiResponseSchema = Shared.createApiResponseSchema(
z.object({ user: User.UserPublicSchema })
);Frontend Usage
With React Hook Form + Zod Resolver
npm install react-hook-form @hookform/resolversimport { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Auth } from 'ufh-schema';
import type { z } from 'zod';
type RegisterInput = z.infer<typeof Auth.RegisterBasicInputSchema>;
function RegisterForm() {
const form = useForm<RegisterInput>({
resolver: zodResolver(Auth.RegisterBasicInputSchema),
defaultValues: {
firstName: '',
lastName: '',
phone: '',
joinReason: '',
},
});
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
{/* Form fields */}
</form>
);
}With TanStack Query
import { useMutation } from '@tanstack/react-query';
import { Auth } from 'ufh-schema';
function useLogin() {
return useMutation({
mutationFn: async (input: z.infer<typeof Auth.LoginInputSchema>) => {
// Validate before sending
const validated = Auth.LoginInputSchema.parse(input);
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(validated),
});
return response.json();
},
});
}Backend Usage
With Express.js
import express from 'express';
import { Auth, Shared } from 'ufh-schema';
const app = express();
app.use(express.json());
// Validation middleware factory
function validate<T>(schema: z.ZodSchema<T>) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
success: false,
error: {
code: 'VALIDATION_ERROR',
issues: result.error.issues,
},
});
}
req.validatedBody = result.data;
next();
};
}
// Use middleware
app.post('/auth/register',
validate(Auth.RegisterBasicInputSchema),
async (req, res) => {
const data = req.validatedBody; // Fully typed!
// Handle registration...
}
);With Hono.js
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { Auth } from 'ufh-schema';
const app = new Hono();
app.post('/auth/login',
zValidator('json', Auth.LoginInputSchema),
async (c) => {
const data = c.req.valid('json'); // Fully typed!
// Handle login...
}
);With NestJS
import { createZodDto } from 'nestjs-zod';
import { Auth } from 'ufh-schema';
// Create DTO from Zod schema
class LoginDto extends createZodDto(Auth.LoginInputSchema) {}
@Controller('auth')
export class AuthController {
@Post('login')
async login(@Body() dto: LoginDto) {
// dto is validated and typed
}
}TypeScript Types
All TypeScript types are pre-exported alongside their Zod schemas. You can import them directly:
import {
Auth,
User,
Survey,
Donation,
Shared
} from 'ufh-schema';
// Types are exported from each schema file
// Auth types
import type { LoginInput } from 'ufh-schema'; // Already exported!
import type { Session } from 'ufh-schema';
// Or access via namespace
type UserPublic = User.UserPublic; // Pre-exported type
type SurveyTemplate = Survey.SurveyTemplate; // Pre-exported type
// You can also use z.infer if you prefer
import type { z } from 'zod';
type LoginInputAlt = z.infer<typeof Auth.LoginInputSchema>;Naming Convention
Each schema file exports:
- Schema:
XyzSchema(Zod schema object) - Type:
Xyz(TypeScript type viaz.infer<>)
| Schema | Type |
|--------|------|
| LoginInputSchema | LoginInput |
| UserPublicSchema | UserPublic |
| SurveyTemplateSchema | SurveyTemplate |
| DonationPublicSchema | DonationPublic |
| ObjectIdSchema | ObjectId |
| EmailSchema | Email |
Best Practices
1. Always Use safeParse for User Input
// ✅ Good - handles errors gracefully
const result = Auth.LoginInputSchema.safeParse(userInput);
if (!result.success) {
handleValidationErrors(result.error);
}
// ❌ Bad - throws on invalid input
const data = Auth.LoginInputSchema.parse(userInput);2. Type Your API Responses
// Define response type using inferred schema
type ApiResponse<T> = {
success: true;
data: T;
} | {
success: false;
error: { code: string; message: string };
};
type LoginResponse = ApiResponse<{
user: z.infer<typeof User.UserPublicSchema>;
tokens: z.infer<typeof Auth.TokenPairSchema>;
}>;3. Extend Schemas When Needed
// Extend existing schema with additional fields
const ExtendedUserSchema = User.UserPublicSchema.extend({
customField: z.string(),
});
// Pick specific fields
const UserSummarySchema = User.UserPublicSchema.pick({
id: true,
firstName: true,
lastName: true,
});
// Make fields optional
const PartialUserSchema = User.UserPublicSchema.partial();4. Use Discriminated Unions for Variants
const ResponseSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('success'), data: DataSchema }),
z.object({ type: z.literal('error'), error: ErrorSchema }),
]);Contributing
- Clone the repository
- Install dependencies:
npm install - Make changes in
src/ - Run checks:
npm run check - Build:
npm run build - Submit PR
Adding New Schemas
- Create schema file in appropriate module directory
- Export from module's
index.ts - Add to this README documentation
- Run
npm run build && npm run check
License
MIT © Hari
