Creating Zod Schemas for Commands and Queries
Introduction
Section titled “Introduction”When you validate Xeno.JS commands and queries with Zod, use ZodUtils to create the schema instead of manually rebuilding the request metadata.
ZodUtils is exported by @xeno-js/shared and provides:
createCommandSchema()for commands;createQuerySchema()for queries.
Both helpers combine your application-specific fields with the Xeno.JS base schema and return a strict Zod object.
This means you do not need to manually add:
intent;type;- query
cacheOptions.
Before you start
Section titled “Before you start”You need:
@xeno-js/shared;zod;- a Command or Query;
- the Zod validation pipeline enabled;
- a schema registered under the same
intentas the request.
For validation configuration, see Validation Pipeline.
Create a Command Schema
Section titled “Create a Command Schema”Use ZodUtils.createCommandSchema() when creating a schema for a Command.
import { z } from 'zod'import { ZodUtils } from '@xeno-js/shared'
const createUserSchema = ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), name: z.string().min(1), },)The second argument is the command-specific Zod shape.
You should not wrap it in z.object().
What ZodUtils adds
Section titled “What ZodUtils adds”The resulting schema includes the fields required by the Xeno.JS command contract:
{ intent: z.literal('CreateUserCommand'), type: z.literal(REQUEST_TYPE.COMMAND), email: z.string().email(), name: z.string().min(1),}You only define the fields belonging to your command.
This avoids repeating request metadata in every schema.
Create a Query Schema
Section titled “Create a Query Schema”Use ZodUtils.createQuerySchema() for a Query.
import { z } from 'zod'import { ZodUtils } from '@xeno-js/shared'
const getUserSchema = ZodUtils.createQuerySchema( 'GetUserQuery', { userId: z.string().uuid(), },)The generated schema includes:
{ intent: z.literal('GetUserQuery'), type: z.literal(REQUEST_TYPE.QUERY), cacheOptions: { cacheKey: z.string().min(1), ttl: z.number().int().positive().optional(), bypassCache: z.boolean().optional(), consistentRead: z.boolean().optional(), isUserScoped: z.boolean() }, userId: z.string().uuid(),}You therefore do not need to add intent, type, or cacheOptions to your application-specific shape.
Add Query Cache Options
Section titled “Add Query Cache Options”A Xeno.JS Query contains cacheOptions.
For example:
import { Query } from '@xeno-js/shared'
export class GetUserQuery extends Query<User> { constructor( public readonly userId: string, ) { super('GetUserQuery', { cacheKey: `user:${userId}`, ttl: 60, bypassCache: false, consistentRead: false, isUserScoped: false }) }}The generated query schema validates these cache options automatically.
Available cache options
Section titled “Available cache options”| Property | Required | Description |
|---|---|---|
cacheKey |
Yes | Key used to identify the cached result. |
ttl |
No | Cache lifetime in seconds. |
bypassCache |
No | Bypasses a cached value for the request. |
consistentRead |
No | Requests a read that bypasses the cached value. |
isUserScoped |
yes | Whether the cache is user-scoped. |
cacheKey must be a non-empty string.
ttl, when provided, must be a positive integer.
Add Only Application-Specific Fields
Section titled “Add Only Application-Specific Fields”The purpose of ZodUtils is to let the schema focus on the data defined by your command or query.
For example, given:
export class CreateUserCommand extends Command<User> { constructor( public readonly email: string, public readonly name: string, ) { super('CreateUserCommand') }}create the schema with:
const createUserSchema = ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), name: z.string().min(1), },)Do not repeat:
intent: z.literal('CreateUserCommand'),type: z.literal(...),Those fields are provided by ZodUtils.
Do Not Pass a Complete z.object()
Section titled “Do Not Pass a Complete z.object()”ZodUtils expects a ZodRawShape, not an already-created Zod object.
Use:
const schema = ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), name: z.string().min(1), },)Not:
const schema = ZodUtils.createCommandSchema( 'CreateUserCommand', z.object({ email: z.string().email(), name: z.string().min(1), }),)The helper creates and extends the base Zod object itself.
Register the Schema
Section titled “Register the Schema”Once the schema is created, register it under the same command or query intent.
For example:
import { z } from 'zod'import { AppBuilder } from '@xeno-js/core'import { ZodUtils } from '@xeno-js/shared'
const createUserSchema = ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), name: z.string().min(1), },)
const getUserSchema = ZodUtils.createQuerySchema( 'GetUserQuery', { userId: z.string().uuid(), },)
const app = new AppBuilder() .addPipeline((config) => { config.validation.zod = { schemas: { CreateUserCommand: createUserSchema, GetUserQuery: getUserSchema, }, } })The schema registry is keyed by request intent.
Therefore:
super('CreateUserCommand')must correspond to:
CreateUserCommand: createUserSchemaLikewise:
super('GetUserQuery', ...)must correspond to:
GetUserQuery: getUserSchemaComplete Command Example
Section titled “Complete Command Example”A command schema can be kept next to the command:
src/└── application/ └── users/ ├── create-user.command.ts └── create-user.schema.tscreate-user.command.ts
Section titled “create-user.command.ts”import { Command } from '@xeno-js/shared'
export class CreateUserCommand extends Command<User> { constructor( public readonly email: string, public readonly name: string, ) { super('CreateUserCommand') }}create-user.schema.ts
Section titled “create-user.schema.ts”import { z } from 'zod'import { ZodUtils } from '@xeno-js/shared'
export const createUserSchema = ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), name: z.string().min(1), },)bootstrap.ts
Section titled “bootstrap.ts”import { AppBuilder } from '@xeno-js/core'import { createUserSchema } from './application/users/create-user.schema'
const app = new AppBuilder() .addPipeline((config) => { config.validation.zod = { schemas: { CreateUserCommand: createUserSchema, }, } })
await app.build()Complete Query Example
Section titled “Complete Query Example”For a query, define the query-specific fields and let ZodUtils add the request and cache fields.
src/└── application/ └── users/ ├── get-user.query.ts └── get-user.schema.tsget-user.query.ts
Section titled “get-user.query.ts”import { Query } from '@xeno-js/shared'
export class GetUserQuery extends Query<User> { constructor( public readonly userId: string, ) { super('GetUserQuery', { cacheKey: `user:${userId}`, ttl: 60, bypassCache: false, consistentRead: false, isUserScoped: false, }) }}get-user.schema.ts
Section titled “get-user.schema.ts”import { z } from 'zod'import { ZodUtils } from '@xeno-js/shared'
export const getUserSchema = ZodUtils.createQuerySchema( 'GetUserQuery', { userId: z.string().uuid(), },)src/bootstrap.ts
Section titled “src/bootstrap.ts”import { AppBuilder } from '@xeno-js/core'import { getUserSchema } from './application/users/get-user.schema'
const app = new AppBuilder() .addPipeline((config) => { config.validation.zod = { schemas: { GetUserQuery: getUserSchema, }, } })
await app.build()Understand Strict Validation
Section titled “Understand Strict Validation”ZodUtils calls .strict() on the resulting schema.
This means properties that are not part of the generated schema are rejected.
For example:
const schema = ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), },)The schema does not accept an arbitrary additional property such as:
{ email: 'john@example.com', unexpectedField: true,}Define every application-specific property that the request is expected to contain.
You should also avoid manually adding the base properties because ZodUtils already provides them.
Troubleshooting
Section titled “Troubleshooting”intent validation fails
Section titled “intent validation fails”Check that the intent used by the request exactly matches the intent passed to ZodUtils.
For example:
super('CreateUserCommand')must use:
ZodUtils.createCommandSchema('CreateUserCommand', ...)Intent matching is exact.
type validation fails
Section titled “type validation fails”Do not manually define the type field.
Use the correct helper:
ZodUtils.createCommandSchema(...)for commands and:
ZodUtils.createQuerySchema(...)for queries.
The helpers add the appropriate request type automatically.
Query validation fails on cacheOptions
Section titled “Query validation fails on cacheOptions”Check that the query provides a cacheOptions object with a non-empty:
cacheKeyFor example:
super('GetUserQuery', { cacheKey: `user:${userId}`,})If ttl is provided, it must be a positive integer.
Validation rejects an unexpected property
Section titled “Validation rejects an unexpected property”The generated schema is strict.
Make sure the property is included in the shape passed to ZodUtils.
For example:
ZodUtils.createQuerySchema('GetUserQuery', { userId: z.string().uuid(),})If the request contains another application-specific property, add it to the shape.
The schema is not being used
Section titled “The schema is not being used”Check:
- the Zod validation pipeline is enabled;
- the schema is registered under the request
intent; - the request’s
intentexactly matches the registry key; - the request is executed through the configured mediator;
- the installed
@xeno-js/sharedversion exposesZodUtils.
See Validation Pipeline for the complete validation configuration.
When to Use ZodUtils
Section titled “When to Use ZodUtils”Use ZodUtils whenever you create a Zod schema for a Xeno.JS CQRS command or query.
It keeps the schema definition focused on application data:
ZodUtils.createCommandSchema( 'CreateUserCommand', { email: z.string().email(), name: z.string().min(1), },)instead of duplicating Xeno.JS request metadata:
z.object({ intent: z.literal('CreateUserCommand'), type: ..., email: z.string().email(), name: z.string().min(1),})For queries, it also supplies the base cacheOptions schema.
Related Documentation
Section titled “Related Documentation”Support Us
Section titled “Support Us”Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section
