Skip to content

Search is only available in production builds. Try building and previewing the site to test it out locally.

Architecture

packages/events/
└── src/
└── users/
└── index.ts # Integration events for the Users domain

Each domain has its own subdirectory. The root index.ts is generated automatically — never edit it by hand.

Every integration event has three layers that work together:

Zod Schema → Inferred type → Event class
(runtime (TypeScript) (@nestjs/cqrs)
validation)
// 1. Reusable base schema (composition)
export const UserPayloadSchema = z.object({
email: z.string().email(),
id: z.string().uuid(),
organizationId: z.string().uuid(),
})
// 2. Event schema (extends the base)
export const UserRegisteredPayloadSchema = UserPayloadSchema.extend({
registeredAt: z.string().datetime(),
})
// 3. Inferred TypeScript type — no duplication
export type UserRegisteredPayload = z.infer<typeof UserRegisteredPayloadSchema>
// 4. Event class for @nestjs/cqrs
export class UserRegisteredEvent {
static readonly EVENT_NAME = 'users.user_registered' as const
readonly payload: UserRegisteredPayload
constructor(raw: unknown) {
// Validates on construction — fails loudly if the payload is invalid
this.payload = UserRegisteredPayloadSchema.parse(raw)
}
}

Why the constructor receives unknown: it forces Zod validation at construction time, both when publishing (emitting module) and when consuming (receiving module from an external broker). The payload never arrives unvalidated.

ElementConventionExample
Directory nameDomain name in pluralusers/, admin/, notifications/
EVENT_NAME<domain>.<entity>_<past_verb>users.user_registered
Base schema<Entity>PayloadSchemaUserPayloadSchema
Event schema<Entity><Action>PayloadSchemaUserRegisteredPayloadSchema
TypeScript type<Entity><Action>PayloadUserRegisteredPayload
Event class<Entity><Action>EventUserRegisteredEvent
ClassEVENT_NAMEPayload
UserRegisteredEventusers.user_registeredid, email, organizationId, registeredAt
UserDeletedEventusers.user_deleteduserId, deletedAt

Publish (emitter):

import { UserRegisteredEvent } from '@frame/events'
await this.eventBus.publish(
new UserRegisteredEvent({
id: user.id,
email: user.email,
organizationId: user.organizationId,
registeredAt: new Date().toISOString(),
})
)

Subscribe (consumer):

import { UserRegisteredEvent } from '@frame/events'
@EventsHandler(UserRegisteredEvent)
export class SendWelcomeEmailHandler implements IEventHandler<UserRegisteredEvent> {
async handle({ payload }: UserRegisteredEvent) {
// payload is typed and already validated by Zod
await this.queue.add('send-welcome-email', payload)
}
}
  1. Create or open the src/<domain>/index.ts file for the corresponding domain. If the domain does not exist, create the directory.

  2. Define the Zod schema for the payload. If the event shares fields with others in the same domain, extract a base schema and use .extend().

  3. Infer the TypeScript type with z.infer<typeof ...>.

  4. Create the event class with a static EVENT_NAME and Zod validation in the constructor.

  5. Publish and subscribe from the corresponding modules in apps/nest.