Modularize schemas by domain
Group related schemas together, perhaps each module or file corresponds to a feature or data type (e.g., user.schema.ts
contains UserSchema, maybe UserUpdateSchema, etc.). This way, you have a clear place to find and update schema definitions when your data model changes.
Reuse and extend schemas
Often you have similar schemas for different situations – for example, a “create user” vs “update user” (where update might allow omission of some fields). Rather than define two from scratch, define a base and derive the other. Zod provides methods like .partial()
to make all fields optional, or .omit()
/ .pick()
to derive a subset. For instance, you might do const UserSchemaPartial = UserSchema.partial()
to get a schema where none of the fields are required (useful for PATCH update input). Or if an extended version has extra fields, use UserSchema.extend({ newField: z.string() })
. This ensures consistency – you’re not redefining the validation rules for those shared fields in multiple places.
Leverage z.infer
for types
Every time you create a schema, you can extract its TypeScript type with z.infer<typeof Schema>
. Use this instead of manually writing an interface or type for the same data shape. This way, if your schema changes, your TypeScript types automatically update to match. For example:
const OrderSchema = z.object({ id: z.string().uuid(), amount: z.number() });
type Order = z.infer<typeof OrderSchema>; // { id: string, amount: number }
Now if you add a field to OrderSchema, the Order type will include it too. This eliminates a whole class of bugs that come from code drifting apart from validation rules.
Compose smaller schemas
If you have common pieces, define them once. Say you have an Address schema used in multiple places, define AddressSchema
and then reuse it: z.object({ address: AddressSchema, … })
. This also applies to enums or literal types that appear in several schemas – define a single schema for the enum and reuse references to it, rather than repeating z.enum([…])
in multiple schemas.
Naming and describing
Use .describe("Description")
on schemas for documentation purposes. While this doesn’t affect validation, it can embed human-readable descriptions in the schema which can later be used for documentation generation or just making error messages clearer. For instance, z.string().min(1).describe("Non-empty name")
. This is useful if you convert schemas to OpenAPI or GraphQL documentation, as the descriptions can carry over.
By structuring schemas thoughtfully, you make it easier to maintain the contract your application relies on. When requirements change (like a new field is added), you update one schema definition in one place. And by inferring types, you ensure all your functions that consume that data type are aware of the change. Essentially, treat schemas as a fundamental part of your application’s architecture – much like you treat database models or API interfaces.