Skip to main content

webiny-event-handler-pattern

EventHandler implementation pattern — handle method, event payloads, filtering, DI, domain event definition, publishing events from UseCases, and reacting to external events. Use this skill to implement any Webiny EventHandler (before/after hooks) or to define and publish your own domain events.

소스 정보

저장소
webiny/webiny-js
최근 소스 활동
2026년 7월 24일 12:20
감지된 SKILL.md 언어
영어
스타
8,049
포크
682

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
webiny-event-handler-pattern
description
EventHandler implementation pattern — handle method, event payloads, filtering, DI, domain event definition, publishing events from UseCases, and reacting to external events. Use this skill to implement any Webiny EventHandler (before/after hooks) or to define and publish your own domain events.
# EventHandler Pattern ## What It Is An **EventHandler** reacts to domain events in the Webiny lifecycle (e.g., `EntryBeforeCreateEventHandler`, `TenantAfterDeleteEventHandler`). Each handler is a DI abstraction with a single `handle` method. ## Naming Convention - `{Entity}Before{Operation}EventHandler` — fires before persistence, can validate/transform/reject - `{Entity}After{Operation}EventHandler` — fires after persistence, for side effects ## Interface Shape ```ts interface SomeEventHandler.Interface { handle(event: SomeEventHandler.Event): Promise<void>; } ``` The `Event` is a `DomainEvent<Payload>` where the payload contains the entity and input data. ## Architecture Rule: Always Wrap Logic in a Reusable Abstraction (MANDATORY) **Never put business logic directly inside an EventHandler.** EventHandlers are thin orchestrators — they receive an event and delegate to an injected service or use case. The real logic lives in a dedicated abstraction. **Why:** Inline handler logic cannot be reused by other handlers, GraphQL resolvers, or CLI commands. **Always follow this structure:** ``` features/ ├── myService/ ← the reusable abstraction │ ├── abstractions.ts │ ├── feature.ts │ └── MyService.ts └── syncOnCreate/ ← thin handler that injects the service ├── feature.ts └── EntryAfterCreateHandler.ts ``` The EventHandler feature and the service feature are **registered separately** in `Extension.ts`. ## How to Implement ```ts import { SomeEventHandler } from "webiny/api/<category>"; import { MyService } from "../myService/abstractions.js"; // ✅ Handler is a thin orchestrator — no business logic here class MyHandler implements SomeEventHandler.Interface { constructor(private myService: MyService.Interface) {} async handle(event: SomeEventHandler.Event) { const { entity, model } = event.payload; // For CMS handlers: ALWAYS filter by model if (model.modelId !== "myModel") return; await this.myService.doWork(entity); } } export default SomeEventHandler.createImplementation({ implementation: MyHandler, dependencies: [MyService] }); ``` See **webiny-api-architect** for how to define `MyService` as a proper abstraction. ## Injecting Dependencies EventHandlers can depend on UseCases, platform services, or custom abstractions: ```ts import { SomeEventHandler } from "webiny/api/<category>"; import { SomeUseCase } from "webiny/api/<category>"; class MyHandler implements SomeEventHandler.Interface { constructor(private someUseCase: SomeUseCase.Interface) {} async handle(event: SomeEventHandler.Event) { const result = await this.someUseCase.execute({/* ... */}); } } export default SomeEventHandler.createImplementation({ implementation: MyHandler, dependencies: [SomeUseCase] }); ``` --- ## Defining Your Own Domain Events When your feature needs to notify other parts of the system about important domain actions, define your own events. ### Event Payload Types (in `abstractions.ts`) Event payloads and handler abstractions live in `abstractions.ts`. The `events.ts` file only contains the event classes. ```ts // features/disableEntity/abstractions.ts import { createAbstraction, type DomainEvent } from "webiny/api"; import type { Entity } from "~/shared/Entity.js"; // Forward declaration — actual classes are in events.ts import type { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js"; // Event Payload Types export interface EntityBeforeDisablePayload { entity: Entity; } export interface EntityAfterDisablePayload { entity: Entity; } // Handler Abstractions — one per event interface IEventHandler<TEvent extends DomainEvent<any>> { handle(event: TEvent): Promise<void>; } export const EntityBeforeDisableEventHandler = createAbstraction< IEventHandler<EntityBeforeDisableEvent> >("MyPackage/EntityBeforeDisableEventHandler"); export namespace EntityBeforeDisableEventHandler { export type Interface = IEventHandler<EntityBeforeDisableEvent>; export type Event = EntityBeforeDisableEvent; } export const EntityAfterDisableEventHandler = createAbstraction< IEventHandler<EntityAfterDisableEvent> >("MyPackage/EntityAfterDisableEventHandler"); export namespace EntityAfterDisableEventHandler { export type Interface = IEventHandler<EntityAfterDisableEvent>; export type Event = EntityAfterDisableEvent; } ``` ### Event Class Definition (`events.ts`) Event classes import payload types and handler abstractions from `abstractions.ts`. ```ts // features/disableEntity/events.ts import { DomainEvent } from "webiny/api"; import { EntityBeforeDisableEventHandler, EntityAfterDisableEventHandler } from "./abstractions.js"; import type { EntityBeforeDisablePayload, EntityAfterDisablePayload } from "./abstractions.js"; export class EntityBeforeDisableEvent extends DomainEvent<EntityBeforeDisablePayload> { eventType = "entity.beforeDisable" as const; getHandlerAbstraction() { return EntityBeforeDisableEventHandler; } } export class EntityAfterDisableEvent extends DomainEvent<EntityAfterDisablePayload> { eventType = "entity.afterDisable" as const; getHandlerAbstraction() { return EntityAfterDisableEventHandler; } } ``` ### Publishing Events from a UseCase ```ts // features/disableEntity/DisableEntityUseCase.ts import { EventPublisher } from "webiny/api"; import { EntityBeforeDisableEvent, EntityAfterDisableEvent } from "./events.js"; class DisableEntityUseCase implements UseCaseAbstraction.Interface { constructor( private eventPublisher: EventPublisher.Interface, private getEntityById: GetEntityByIdUseCase.Interface, private updateEntity: UpdateEntityUseCase.Interface ) {} async execute(entityId: string): Promise<Result<void, UseCaseAbstraction.Error>> { const getResult = await this.getEntityById.execute(entityId); if (getResult.isFail()) return Result.fail(getResult.error); const entity = getResult.value; // Publish BEFORE event (can be intercepted to reject) await this.eventPublisher.publish(new EntityBeforeDisableEvent({ entity })); // Perform the operation const updateResult = await this.updateEntity.execute(entityId, { status: "disabled" }); if (updateResult.isFail()) return Result.fail(updateResult.error); // Publish AFTER event (for side effects) await this.eventPublisher.publish(new EntityAfterDisableEvent({ entity: updateResult.value })); return Result.ok(); } } export default UseCaseAbstraction.createImplementation({ implementation: DisableEntityUseCase, dependencies: [EventPublisher, GetEntityByIdUseCase, UpdateEntityUseCase] }); ``` --- ## Reacting to External Events To react to events from other packages (e.g., CMS entry deletion), implement the external event's handler abstraction: ```ts // features/cleanupOnEntryDelete/CleanupOnEntryDeleteHandler.ts import { EntryAfterDeleteEventHandler } from "webiny/api/cms/entry"; import { CleanupService } from "../cleanupService/abstractions.js"; import { MY_MODEL_ID } from "~/shared/constants.js"; class CleanupOnEntryDeleteHandler implements EntryAfterDeleteEventHandler.Interface { constructor(private cleanupService: CleanupService.Interface) {} async handle(event: EntryAfterDeleteEventHandler.Event): Promise<void> { const { entry, model } = event.payload; // ALWAYS filter by model — handler fires for ALL models if (model.modelId !== MY_MODEL_ID) return; if (!event.payload.permanent) return; await this.cleanupService.cleanup(entry.entryId); } } export default EntryAfterDeleteEventHandler.createImplementation({ implementation: CleanupOnEntryDeleteHandler, dependencies: [CleanupService] }); ``` --- ## Event Naming Conventions | Artifact | Pattern | Example | | ------------------- | ------------------------------------------------ | --------------------------------- | | `eventType` | `"entity.beforeAction"` / `"entity.afterAction"` | `"tenant.beforeDisable"` | | Handler abstraction | `{Entity}{Before\|After}{Action}EventHandler` | `TenantBeforeDisableEventHandler` | | Event class | `{Entity}{Before\|After}{Action}Event` | `TenantBeforeDisableEvent` | ## Registration **YOU MUST include the full file path with the `.ts` extension in the `src` prop.** For example, use `src={"@/extensions/my-handler.ts"}`, NOT `src={"@/extensions/my-handler"}`. Omitting the file extension will cause a build failure. **YOU MUST use `export default` for the `createImplementation()` call** when the file is targeted directly by an Extension `src` prop. Using a named export (`export const Foo = SomeFactory.createImplementation(...)`) will cause a build failure. Named exports are only valid inside files registered via `createFeature`. ```tsx <Api.Extension src={"@/extensions/my-handler.ts"} /> ``` Deploy with: `yarn webiny deploy api --env=dev` ## Resolving Types (MANDATORY) **Before writing any code that accesses event payload properties or domain types (CmsEntry, CmsModel, etc.), you MUST read the source file listed in the catalog's `Source` field to verify the exact property names and types. Do not assume or guess property names from memory.** 1. Read the `abstractions.ts` file from the catalog `Source` path — it contains the payload interface 2. Read the `events.ts` file (sibling to `abstractions.ts`) — it contains the `Interface` and `Event` types 3. If the payload references domain types, follow the import and read that declaration ## Key Rules - **Before handlers**: payload may be mutable — write to it to set computed fields. Throw to reject the operation. - **After handlers**: payload reflects persisted state — do not mutate. Use for side effects. - **Filter by entity**: handlers fire for ALL entities of that type. Always check `modelId`, `entity type`, etc. - **Events extend** `DomainEvent<TPayload>` with `eventType` property (not `static type`) - **Every event must** implement `getHandlerAbstraction()` returning its handler abstraction - **Every handler abstraction** must have a namespace with `Interface` and `Event` types - **Payload types** live in `abstractions.ts`; event classes live in `events.ts` - **Publish order**: before event → operation → after event - DI constructor parameter order must match the `dependencies` array order exactly - Use `.js` extensions in import paths (ES modules) ## Related Skills - **webiny-api-architect** — Architecture overview, Services vs UseCases, feature naming - **webiny-use-case-pattern** — UseCase implementation where events are published from - **webiny-dependency-injection** — Injectable services catalog
GitHub에서 보기