0%
balqalam Logo
balqalam
FeaturesCommunityPricingDocumentation
Login
  • Introduction
  • Pitch
  • Hogwarts
  • Live Demo
  • MVP
  • Roadmap
  • Launch Sprint
  • PRD
  • Get Started
  • Localhost
  • Architecture
  • Structure
  • Pattern
  • Page
  • Layout
  • Content
  • Types
  • Config
  • Actions
  • Queries
  • Authorization
  • Validation
  • Form
  • Table
  • Detail
  • Card
  • Util
  • Hooks
  • List Params
  • Views
  • README.md
  • ISSUE.md
  • Technology Stack
  • Database
  • File
  • CDN Assets
  • Entry Points
  • Dashboard
  • Authentication
  • Credentials
  • OAuth
  • Flow Diagrams
  • Multi-Tenancy
  • Offline
  • Onboarding
  • Onboarding Videos
  • Add Values
  • Admission
  • Application
  • Attendance
  • Compliance
  • Profile
  • Exams
  • Exam Wizard
  • Timetable
  • Classrooms
  • Notifications
  • Conference
  • LMS (Lumos)
Finance
  • Finance
  • Fee Management
  • Invoice
  • Wallet
  • Salary
  • Payroll
  • Timesheet
  • Expenses
  • Budget
  • Receipt
  • Accounts
  • Banking
  • Reports
  • Dashboard
  • Permissions
  • Messages
  • Integration Flow
  • Provision
  • AI Document Processing
  • Document Intelligence
  • Internationalization
  • Translation
  • Translation Guide
  • Icons
  • Docs Factory
  • Inspiration
  • Listings
  • Teachers
  • Students
  • Catalog
  • Library
  • Contributing
  • Code of conduct
  • GitHub Workflow
  • Database Seeds
  • Database Safety
  • Test Accounts
  • Playwright
  • Prettier
  • Block Rebound
Sales & GTM
  • Marketing Brief
  • Sales
  • Go-to-market
  • Marketing
  • Admission — Feature Spotlight
  • Pilot Program
  • Leads
  • Proposal
  • Outreach Templates
  • Case Study
  • Competitors
  • Landing-page teardown
  • Competitor FAQ
  • Business model
  • Shared economy
  • Traction
Fundraising & Ecosystem
  • Get Support
  • Investor Deck
  • Data Room
  • Investors
  • Accelerators
  • Incubators
  • Grants
  • Sponsors
  • Partners
  • Competitions & Hackathons
  • Universities & Training Centers

Validation

PreviousNext

Zod schemas — the single source of truth for input shapes.

validation.ts is the single source of truth for input shapes. Server actions and forms import from here — schemas don't live anywhere else.

Categories

CategoryShapeReference
CRUD entitycreateSchema + updateSchema (.partial().extend({ id })) + filterSchemalibrary/validation.ts
Multi-step formOne schema per step + combined wizard schema with .refine()onboarding/validation.ts
Search and filtersnuqs + Zod for URL query statelistings/students/list-params.ts
AuthLogin, register, reset, 2FA — often i18n factoriesauth/login/validation.ts
SettingsPartial updates of a config recordschool-dashboard/settings/validation.ts
DomainDomain-specific shapes (timetable conflict, exam answer)timetable/validation.ts

Rules

#Rule
1Co-export schema + inferred type — xSchema and type XInput = z.infer<typeof xSchema>
2Named exports only
3Use shared primitives from src/lib/validation/primitives.ts
4Compose with .extend(), .partial(), .pick()
5.refine() for cross-field validation
6i18n factory pattern when error messages are user-facing
7Stay under ~300 lines — split by concept

Canonical example

import { z } from "zod"
 
export const subjectCreateSchema = z.object({
  subjectName: z.string().min(1, "Subject name is required").max(100),
  departmentId: z.string().cuid(),
  description: z.string().max(500).optional(),
  active: z.boolean().default(true),
})
export type SubjectCreateInput = z.infer<typeof subjectCreateSchema>
 
export const subjectUpdateSchema = subjectCreateSchema.partial().extend({
  id: z.string().cuid(),
})
export type SubjectUpdateInput = z.infer<typeof subjectUpdateSchema>
 
export const subjectFilterSchema = z.object({
  q: z.string().optional(),
  departmentId: z.string().cuid().optional(),
  active: z.boolean().optional(),
})
export type SubjectFilter = z.infer<typeof subjectFilterSchema>

i18n factory

For translated error messages, accept the dictionary slice and return the schema:

export function createTitleSchema(d: Dictionary["onboarding"]["title"]) {
  return z.object({
    name: z.string().min(1, d.errors.required).max(100, d.errors.tooLong),
  })
}
export type TitleInput = z.infer<ReturnType<typeof createTitleSchema>>

Shared primitives

Lifted to src/lib/validation/primitives.ts:

export const emailSchema = z.string().email()
export const phoneSchema = z
  .string()
  .regex(/^\+?[1-9]\d{1,14}$/, "Invalid phone")
export const slugSchema = z
  .string()
  .regex(/^[a-z0-9-]+$/, "Lowercase letters, numbers, hyphens only")
export const colorSchema = z
  .string()
  .regex(/^#[0-9a-f]{6}$/i, "Hex color (#RRGGBB)")
export const nameSchema = z.string().min(1).max(100)

Anti-patterns

  • Inline schemas in actions.ts (14 files) — move to validation.ts.
  • Inline schemas in form.tsx (15 files) — same fix.
  • z.any() / z.unknown() (20 occurrences) — replace with the actual shape.
  • Bare .min(1) without an error message (149 occurrences).
  • Duplicate primitives — 6 email regexes, 8 phone patterns. Lift to primitives.ts.
  • Duplicate schemas across features — extract to src/components/<shared>/validation.ts.
  • Bloated files — onboarding/validation.ts (659), letters/validation.ts (546). Split.

Naming

  • File: validation.ts.
  • Suffixes: <Name>Input for create/update, <Name>Filter for filter shapes. Don't mix FormData, SchemaType, Schema.

Sibling roles

  • actions.ts imports <x>Schema and validates with safeParse.
  • form.tsx imports <x>Schema + <x>Input for useForm({ resolver: zodResolver }).
  • types.ts imports <x>Input for non-form references.
  • queries.ts rarely imports — operates on Prisma types.

See also

  • Pattern
  • Actions
  • Form
  • Types
AuthorizationForm

On This Page

CategoriesRulesCanonical examplei18n factoryShared primitivesAnti-patternsNamingSibling rolesSee also

Built by Databayt ·

Welcome to balqalam.

A great journey is about to begin.