Litestore · Developer docs
Data access

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

GET/api/products?q=linen&limit=12
AccessNo key needed
NotesDescribed by /api/openapi.json
Every form that accepts an email address runs the same shared Email schema before Litestore stores the value.

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.

ExportWhat it checks
Email, OptionalEmailThe schema trims and lowercases the value, then checks the email format. The optional version also accepts "".
OptionalPhoneA 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, OptionalUrlThe value has to be a URL that can be parsed. The optional version also accepts "".
SlugA 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:

platform/field-schemas.ts
// 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 "----".

platform/field-schemas.ts
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.

modules/creators/links/schema.ts
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 needsWhere it's defined
Batch ids for archive, restore and delete actionsbatchIdsSchema in platform/crud/types.ts (at most 500)
Table filtering, sorting and pagination paramsplatform/table-params.ts and platform/crud/pagination.ts
A module's filter parametersThat 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.

On this page