Agent Skillswebiny/webiny-js › webiny-event-handler-pattern

webiny-event-handler-pattern

GitHub

Webiny EventHandler实现规范,指导如何定义、发布及响应领域事件。强调Handler作为薄编排层,业务逻辑需封装在可复用抽象中,支持DI和事件过滤。

skills/user-skills/api/event-handler-pattern/SKILL.md webiny/webiny-js

Trigger Scenarios

需要实现Webiny生命周期钩子(如Before/After) 需要定义和发布自定义领域事件 处理事件驱动的业务逻辑架构

Install

npx skills add webiny/webiny-js --skill webiny-event-handler-pattern -g -y
More Options

Non-standard path

npx skills add https://github.com/webiny/webiny-js/tree/next/skills/user-skills/api/event-handler-pattern -g -y

Use without installing

npx skills use webiny/webiny-js@webiny-event-handler-pattern

指定 Agent (Claude Code)

npx skills add webiny/webiny-js --skill webiny-event-handler-pattern -a claude-code -g -y

安装 repo 全部 skill

npx skills add webiny/webiny-js --all -g -y

预览 repo 内 skill

npx skills add webiny/webiny-js --list

SKILL.md

Frontmatter
{
    "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

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

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:

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.

// 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.

// 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

// 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:

// 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.

<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

Version History

  • 80eb1c5 Current 2026-08-20 10:07

Same Skill Collection

.claude/skills/grill-me/SKILL.md
.claude/skills/prd-to-plan/SKILL.md
.claude/skills/preflight/SKILL.md
.claude/skills/tester/SKILL.md
.claude/skills/write-a-prd/SKILL.md
skills/repo-skills/add-feature-flag/SKILL.md
skills/user-skills/admin/admin-architect/SKILL.md
skills/user-skills/admin/admin-permissions/SKILL.md
skills/user-skills/admin/form-model/SKILL.md
skills/user-skills/admin/new-entry-wizard/SKILL.md
skills/user-skills/admin/website-builder/page-settings/SKILL.md
skills/user-skills/admin/website-builder/wb-preview-url-modifier/SKILL.md
skills/user-skills/api-bundle-size-limit/SKILL.md
skills/user-skills/api/api-architect/SKILL.md
skills/user-skills/api/cms-bulk-actions/SKILL.md
skills/user-skills/api/custom-field-type/SKILL.md
skills/user-skills/api/graphql-api/SKILL.md
skills/user-skills/api/http-route/SKILL.md
skills/user-skills/api/permissions/SKILL.md
skills/user-skills/api/use-case-pattern/SKILL.md
skills/user-skills/api/v5-to-v6-migration/SKILL.md
skills/user-skills/api/websocket-notifications/SKILL.md
skills/user-skills/cli-extensions/SKILL.md
skills/user-skills/configure-auth0/SKILL.md
skills/user-skills/configure-entraid/SKILL.md
skills/user-skills/configure-okta/SKILL.md
skills/user-skills/dependency-injection/SKILL.md
skills/user-skills/full-stack-architect/SKILL.md
skills/user-skills/generated/api/aco/SKILL.md
skills/user-skills/generated/api/cms/SKILL.md
skills/user-skills/generated/api/file-manager/SKILL.md
skills/user-skills/generated/api/scheduler/SKILL.md
skills/user-skills/generated/api/security/SKILL.md
skills/user-skills/generated/api/system/SKILL.md
skills/user-skills/generated/api/tenancy/SKILL.md
skills/user-skills/generated/api/tenant-manager/SKILL.md
skills/user-skills/generated/api/website-builder/SKILL.md
skills/user-skills/generated/infra/SKILL.md
skills/user-skills/infrastructure-extensions/SKILL.md
skills/user-skills/local-development/SKILL.md
skills/user-skills/mailer-smtp/SKILL.md
skills/user-skills/project-structure/SKILL.md
.claude/skills/webiny-skill-creator/SKILL.md
skills/user-skills/admin/ui-extensions/SKILL.md
skills/user-skills/api/ai-powerups-content/SKILL.md
skills/user-skills/cognito-federation/SKILL.md
skills/user-skills/content-models/SKILL.md
skills/user-skills/generated/admin/aco/SKILL.md
skills/user-skills/generated/admin/ai-powerups/SKILL.md

Metadata

Files
0
Version
80eb1c5
Hash
f077cdb6
Indexed
2026-08-20 10:07

inicio - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-22 03:53
浙ICP备14020137号-1 $mapa de visitantes$