Contracts
Reuse Litestore's shared Zod field validators when you write the schema for a new module.
Litestore keeps one shared Zod validator for each common kind of field, such as an email address or a slug, so every form checks it the same way. Read this page when you write a schema for a new admin or storefront module.
Request
Field validators
All shared field validators are in one file, platform/field-schemas.ts. It is about 40 lines long, and every entity schema imports from it instead of typing the rule out again.
| Export | What it checks |
|---|---|
Email, OptionalEmail | The schema trims and lowercases the value, then checks the email format. The optional version also accepts "". |
OptionalPhone | A phone number can be at most 20 characters long. It may contain digits, spaces, dashes, parentheses and a leading +, and it must contain at least 7 actual digits. It also accepts "". |
Url, OptionalUrl | The value has to be a URL that can be parsed. The optional version also accepts "". |
Slug | A slug is 1 to 250 characters long and has to match the pattern below. |
This is the slug pattern:
^[a-z0-9]+(?:-[a-z0-9]+)*$Email and Phone
Email normalizes the address before it validates it:
// Every email is trimmed + lowercased BEFORE validation, so the whole app stores
// one canonical form. Without this, a signup `John@X.com` and an admin-created
// `john@x.com` are two rows Postgres's case-sensitive unique index never dedupes.
const normalizedEmail = z.string().trim().toLowerCase()
export const Email = normalizedEmail.email("Please enter a valid email address")Normalizing before validating means every address reaches the database in the same form, so two spellings of one address can't end up as two rows.
The phone rule also counts the digits, because its character regex on its own would accept values like "+++", "((((" and "----".
const Phone = z
.string()
.trim()
.max(20, "Phone number is too long")
.regex(/^[+]?[\d\s\-().]+$/, "Please enter a valid phone number")
// 7 is the shortest real subscriber number (E.164 national minimum).
.refine(value => (value.match(/\d/g)?.length ?? 0) >= 7, "Please enter a valid phone number")
export const OptionalPhone = Phone.optional().or(z.literal(""))Import the validator, don't retype it
A local z.string().email() skips the trim-and-lowercase step, so it can write a second row for an existing address that Postgres's case-sensitive unique index won't catch. The shared validators exist because the codebase once had seven different versions of the email check and five of the URL check.
Module schemas
Imports
A module's entity schema imports the validators it needs and builds everything else with plain Zod.
Field types
You use the validator directly, and refine it further when the column is narrower.
import { z } from "zod"
import { Slug, Url } from "~/platform/field-schemas"
export const linkSchema = z.object({
slug: Slug,
destination: Url.max(2000),
utmSource: z.string().max(100).optional().nullable(),
utmMedium: z.string().max(100).optional().nullable(),
utmCampaign: z.string().max(100).optional().nullable(),
campaignId: z.string().cuid().optional().nullable(),
productId: z.string().cuid().optional().nullable(),
isActive: z.boolean().default(true),
// …
})CRUD schema
Set this schema as the schema field on the module's BaseCRUD subclass, and use it as the .input() of the module's create and update actions. For a partial update, build the schema with partialUpdateSchema() from platform/crud/partial-update-schema.ts, which drops create-time defaults.
Around fifteen files import from ~/platform/field-schemas. Most of them are admin and storefront schemas, and a handful of actions.ts files use a validator to check a single argument.
Other shared shapes
| What the app needs | Where it's defined |
|---|---|
| Batch ids for archive, restore and delete actions | batchIdsSchema in platform/crud/types.ts (at most 500) |
| Table filtering, sorting and pagination params | platform/table-params.ts and platform/crud/pagination.ts |
| A module's filter parameters | That module's schema.ts |
Choosing a validator
Before you add a new schema, check whether a shared validator already covers each rule, so that a later change only has to be made in one place.
Related
CRUD
An admin module saves, archives and deletes records through the shared BaseCRUD class.
Overview
Admin and storefront writes use zsa server actions, service-key callers use oRPC at /api/rpc, and /api/openapi.json describes the REST endpoints.
Schema
The core Prisma models follow shared conventions, and schema changes follow a few rules to stay safe.