---
name: catalyst-handler-composer
description: >
  Fill in business logic inside CLI-generated NestJS handlers in the Aurora Catalyst backend,
  refine the controller/resolver/handler scaffolds the CLI produces from `additionalApis`
  declarations, AND compose hand-written COMPOSITE HANDLERS for protocol-flow endpoints (e.g.,
  OAuth token issuance dispatching PASSWORD vs REFRESH_TOKEN variants to sub-handlers). The skill
  is CLI-first for aggregates: `aurora-catalyst-cli` generates the handler, service, controller,
  resolver, DTO, and GraphQL files from the module's `*.aurora.yaml`. For hand-written protocol
  flows, see the "Composite handlers" section below — the dispatch lives in the handler, NEVER
  duplicated in the transports. Trigger: editing any *.handler.ts under
  @app/<pkg>/<module>/application/<op>/, adding pre-persistence validation or business rules
  inside an existing handler's main(), wiring @EmitEvent / @ApplySchema / @Format decorators,
  refining the payload types / auth permission / return shape of a scaffolded additionalApis
  handler, deciding between handler and service for where logic belongs, OR composing a
  composite/dispatch handler that fronts a protocol-flow endpoint exposed over multiple transports.
  Fires even when the user says only "write a handler", "implement the business logic for X", or
  "the OAuth flow needs a handler" — Step 0 of SKILL.md routes the CLI-territory case through the
  CLI if the target file does not yet exist; the Composite handlers section routes the
  hand-written protocol-flow case. Does NOT apply to guards, interceptors, pipes, or filters (use
  `catalyst-nestjs-primitives`) and does NOT replace full-module audits (use
  `catalyst-review-module`).
license: MIT
metadata:
  author: aurora
  version: '1.0'
---

## Step 0 — CLI-First Gate (Read Before Anything Else)

Before touching any file, answer: **"should I be writing this file by hand at all?"** Catalyst's
backend is CLI-generated end-to-end. The `aurora-catalyst-cli` emits every standard artifact —
controller, resolver, handler, service, DTO, GraphQL schema — from the module's YAML and keeps them
regenerable. Hand-writing what the CLI would produce means fighting the generator: the next
`catalyst load` either overwrites your file or emits a `.origin` conflict.

Route every handler task through one of these three branches before reading the pre-flight checklist
below.

```
┌─────────────────────────────────────────────────────────────────────┐
│  What do you actually need?                                         │
└────────────────────────────┬────────────────────────────────────────┘
                             │
   ┌─────────────────────────┼─────────────────────────┐
   │                         │                         │
   ▼                         ▼                         ▼
┌──────────────┐   ┌───────────────────────┐   ┌──────────────────┐
│ Standard     │   │ Custom verb for a     │   │ Hand-write a     │
│ CRUD op      │   │ domain-specific       │   │ file from        │
│ (create,     │   │ action (activate,     │   │ scratch — no     │
│  update,     │   │ approve, cancel,      │   │ YAML involvement │
│  find, get,  │   │ change-status,        │   │                  │
│  paginate,   │   │ check-unique-*, ...)  │   │                  │
│  delete)     │   │                       │   │                  │
└──────┬───────┘   └───────────┬───────────┘   └─────────┬────────┘
       │                       │                         │
       ▼                       ▼                         ▼
Edit YAML              Declare in YAML              STOP.
(operations /          `additionalApis`.            Surface to user.
 excludedOperations    Run catalyst load.           Almost always
 / excludedFiles).     CLI scaffolds 3 files.       the wrong path.
Run catalyst load.     Refine the scaffold          If truly custom
CLI (re)generates      (types → auth → return       and CLI can't
the files.             → services → logic).         help, document
                                                    why in the user
Read                   Read                         conversation.
`references/           `references/
 cli-first-            additional-apis-
 workflow.md`.         workflow.md`.
```

**The regeneration command** (same in all branches):

```bash
catalyst load back module --name=<bounded-context>/<module> --force
```

Full command documentation, YAML switches, and lockfile interaction:
[`references/cli-first-workflow.md`](references/cli-first-workflow.md).

**Scaffold refinement details** (the five phases, mutation vs query, tenant/auditing branches):
[`references/additional-apis-workflow.md`](references/additional-apis-workflow.md).

Only after Step 0 has routed the task — and the target file exists on disk — proceed to the
pre-flight checklist and the archetype-specific references below.

## When to Use

- Editing an existing handler's `main()` to add business validation, pre-persistence checks,
  domain-specific orchestration, or event emission.
- Wiring `@ApplySchema` (write-side validation), `@EmitEvent` (event topic emission), or `@Format`
  (read-side response shaping) on a handler.
- Refining a CLI-scaffolded `additionalApis` handler (payload type, auth permission, return type,
  service injection, business logic).
- Deciding whether logic belongs in the handler or in the service (both have a `main()` method, but
  they play different roles — see the decision tree below).

This skill is **COMPOSE-only**. For auditing an existing handler or a whole module, use
`catalyst-review-module` — handler-level audit happens as part of the module audit, not in
isolation.

## Pre-flight Checklist (Blocking)

**Prerequisite**: Step 0 has already routed the task through the CLI and the target handler file
exists on disk. If the file does not exist, return to Step 0 — the CLI produces it, not this table.
Hand-writing the file bypasses the generator and breaks on the next regeneration.

With the file in hand, match its name against this table and read the referenced file in full. Each
reference anchors its example in the real `iam/tag` module (CRUD archetypes) or in the real
`iam/role/inherit-permissions-role` scaffold (`additionalApis` archetype). The references hold
decorator wiring, parameter shape, and error-handling details that do not fit inside this file.

| Target file                         | Reference to read                                                                |
| ----------------------------------- | -------------------------------------------------------------------------------- |
| `*-create-*.handler.ts`             | [references/create-handler.md](references/create-handler.md)                     |
| `*-update-*.handler.ts`             | [references/update-handler.md](references/update-handler.md)                     |
| `*-find-*-by-id.handler.ts`         | [references/read-handler.md](references/read-handler.md)                         |
| `*-find-*.handler.ts` (no `-by-id`) | [references/read-handler.md](references/read-handler.md)                         |
| `*-get-*.handler.ts`                | [references/read-handler.md](references/read-handler.md)                         |
| `*-paginate-*.handler.ts`           | [references/read-handler.md](references/read-handler.md)                         |
| `*-delete-*.handler.ts`             | [references/delete-handler.md](references/delete-handler.md)                     |
| `additionalApis`-scaffolded handler | [references/additional-apis-workflow.md](references/additional-apis-workflow.md) |

## Where Does the Logic Go?

Both handler and service expose a `main()` method. They are not interchangeable. The split is what
keeps the service thin and regenerable, and the handler expressive of the domain.

```
┌─────────────────────────────────────────────────────────────┐
│  Kind of logic                          │  Where it goes    │
├─────────────────────────────────────────────────────────────┤
│  Business validation (pre-check)        │  Handler.main()   │
│  Repository lookup before action        │  Handler.main()   │
│  Domain rule (status transition, etc.)  │  Handler.main()   │
│  Error throwing (NotFound, Conflict)    │  Handler.main()   │
│  Persistence (create/update/delete)     │  Service.main()   │
│  Aggregate composition from payload     │  Service.main()   │
│  Repository writes                      │  Service.main()   │
└─────────────────────────────────────────────────────────────┘
```

The handler orchestrates and validates. The service persists. Event emission is declarative via
`@EmitEvent` on the handler — the service does not touch the event bus.

## Composite handlers — one business flow, two transports

A **composite handler** is a hand-written handler that fronts a protocol-flow endpoint exposed
over MULTIPLE transports (REST + GraphQL, REST + gRPC, etc.). Its job is to:

1. Receive a single polymorphic input.
2. Dispatch to a sub-handler based on a discriminator field (typically a `grantType`,
   `eventType`, `action`, etc.).
3. Return a single response shape that the transports map to their on-wire format.

The transports (controller, resolver) **MUST NOT replicate the dispatch logic**. Each transport
parses its own on-wire shape into the composite handler's input type, calls `composite.main(input)`,
and returns the response. The handler owns the business decision; the transports own the
parsing/mapping.

### Anatomy

```
@app/<bc>/<flow>/
└── application/
    ├── <flow>.handler.ts                    ← COMPOSITE — dispatches by discriminator
    ├── <flow>-response.types.ts             ← polymorphic input + response types
    ├── <variant-a>/<flow>-<variant-a>.handler.ts   ← SUB-HANDLER — variant-specific
    └── <variant-b>/<flow>-<variant-b>.handler.ts   ← SUB-HANDLER — variant-specific
```

### Shape of the composite

```typescript
@Injectable()
export class <Flow>Handler {
  constructor(
    private readonly <variantA>Handler: <Flow><VariantA>Handler,
    private readonly <variantB>Handler: <Flow><VariantB>Handler,
  ) {}

  async main(input: <Flow>Input): Promise<<Flow>Response> {
    switch (input.<discriminator>) {
      case '<VARIANT_A>':
        return this.<variantA>Handler.main(mapToVariantAInput(input));
      case '<VARIANT_B>':
        return this.<variantB>Handler.main(mapToVariantBInput(input));
      default:
        throw new Unsupported<Discriminator>Error();
    }
  }
}
```

### Shape of the transports — slim, parse-only

REST controller:

```typescript
@Controller('<flow>')
export class <Flow>Controller {
  constructor(private readonly handler: <Flow>Handler) {}

  @Post('<endpoint>')
  async <action>(@Body() body: Record<string, unknown>, /* other transport hooks */): Promise<<Response>> {
    const input = parseRestBody(body /* + headers/context */);
    return this.handler.main(input);
  }
}
```

GraphQL resolver:

```typescript
@Resolver()
export class <Flow>Resolver {
  constructor(private readonly handler: <Flow>Handler) {}

  @Mutation('<mutationName>')
  async <action>(@Args('payload') payload: <GraphQLInput>, @Context() ctx: any): Promise<<GraphQLOutput>> {
    const input = mapGraphqlPayloadToInput(payload, ctx);
    const response = await this.handler.main(input);
    return mapResponseToGraphql(response);
  }
}
```

Neither transport checks the discriminator. Neither knows what variants exist. They MUST be
ignorant of the dispatch — that's what makes "one business flow, two transports" work.

### Antipattern detection — dispatch duplicated across transports

If your REST controller AND your GraphQL resolver both contain logic that looks like:

```typescript
if (payload.<discriminator> === '<VARIANT_A>') {
  return this.<variantA>Handler.main(...);
}
if (payload.<discriminator> === '<VARIANT_B>') {
  return this.<variantB>Handler.main(...);
}
throw new UnsupportedError();
```

…STOP. This is the antipattern. The dispatch belongs in a composite handler, not in the
transports. To refactor:

1. Create `<flow>.handler.ts` with the dispatch.
2. Move the sub-handler injections from the controller/resolver to the composite handler.
3. Reduce the controller/resolver to: parse → call `composite.main(input)` → return.
4. Update tests: the composite handler's spec asserts the dispatch table; the transport specs
   assert ONLY their parsing/mapping.

### Where the composite handler lives

Per `catalyst-project-structure` "Hand-written protocol-flow endpoints" section:

- Composite handler: `@app/<bc>/<flow>/application/<flow>.handler.ts`
- Sub-handlers: `@app/<bc>/<flow>/application/<variant>/<flow>-<variant>.handler.ts`
- Composite registered as provider in the FLOW's dedicated module (`@api/<bc>/<flow>/<flow>.module.ts`),
  NOT in the Cliter-generated `<bc>.module.ts`.

### Worked example — `IssueTokenHandler` (OAuth2)

```typescript
// @app/o-auth/issue-token/application/issue-token.handler.ts
@Injectable()
export class IssueTokenHandler {
  constructor(
    private readonly passwordHandler: IssueTokenPasswordHandler,
    private readonly refreshHandler: IssueTokenRefreshHandler,
  ) {}

  async main(input: IssueTokenInput): Promise<IssueTokenResponse> {
    if (input.grantType === 'PASSWORD') {
      return this.passwordHandler.main({ /* mapped to password input */ });
    }
    if (input.grantType === 'REFRESH_TOKEN') {
      return this.refreshHandler.main({ /* mapped to refresh input */ });
    }
    throw new UnsupportedGrantTypeError();
  }
}
```

Both `IssueTokenController` (REST, parses `application/x-www-form-urlencoded` + Basic auth) and
`IssueTokenResolver` (GraphQL, parses typed `OAuthCreateCredentialsInput`) call
`IssueTokenHandler.main(input)`. Neither knows about `passwordHandler` or `refreshHandler`.



Minimal shape from `iam/tag`:

```typescript
@Injectable()
export class IamCreateTagHandler {
  constructor(
    private readonly eventEmitter: EventEmitter2,
    private readonly createService: IamCreateTagService,
    private readonly findByIdService: IamFindTagByIdService,
  ) {}

  @EmitEvent('iam.tag.created')
  @ApplySchema(IamTagFieldSchema)
  async main(payload: IamCreateTagInput, handlerMeta?: HandlerMeta): Promise<IamTag> {
    // pre-persistence validation would sit here, before the service call
    await this.createService.main(payload, handlerMeta);
    return await this.findByIdService.main(payload.id, {}, handlerMeta);
  }
}
```

## Anti-patterns

Each smell below ports cleanly onto the catalyst architecture. If you recognize one in
training-memory instincts, reach for the alternative instead.

1. **`@CommandHandler` / `@QueryHandler` + `execute()` method.** → Use `@Injectable()` + `main()`.
   Catalyst does not use `@nestjs/cqrs`; there is no `ICommandHandler` / `ICommandBus` in the
   codebase.

2. **Injecting `ICommandBus` / `IQueryBus` to dispatch from a resolver or controller.** → The `@api`
   resolver/controller holds a reference to the handler directly and calls
   `this.handler.main(payload, { timezone, repositoryOptions: { auditing } })`. There is no bus.

3. **Constructing Value Objects inside the handler.** → Use `@ApplySchema(FieldSchema)` and let the
   `FieldSchema` declare validation (nullable, maxLength, rules, etc.). The decorator validates the
   payload before `main()` runs. See
   [`catalyst-field-schema/references/field-schema-and-decorators.md`](../catalyst-field-schema/references/field-schema-and-decorators.md)
   for the full `FieldSchema` interface, the 16 type handlers, and the `@Sanitize` / `@Format` /
   `@ApplySchema` decision tree.

4. **`aggregate.commit()` for event emission.** → Use `@EmitEvent('pkg.entity.action')` on the
   handler. The decorator publishes to the `EventEmitter2` after a successful `main()`. The handler
   still receives `EventEmitter2` via DI because the decorator reads it from the instance, but you
   do not call it yourself.

5. **`/* #region AI-generated code */` markers in TypeScript.** → Omit them. Catalyst's
   preservation-regions engine parses HTML only (`<!-- #region AURORA:<NAME>-START -->`); TypeScript
   support has been deferred. A marker in TS gives false safety — regeneration ignores it and the
   custom code is overwritten.

6. **Hardcoded enum values as string literals.** → Import the enum type from
   `@app/<pkg>/<module>/domain/` (or wherever the YAML-generated enum lives) and use the typed
   constant. String literals drift silently when the YAML changes; typed enums break the build
   loudly.

7. **Hand-writing a controller, resolver, or handler file the CLI would generate.** → This is the
   Step 0 violation. If the operation is a standard CRUD, edit the module's YAML (`operations` /
   `excludedOperations` / `excludedFiles`) and run
   `catalyst load back module --name=<bc>/<module> --force`. If the operation is a custom verb,
   declare it in `additionalApis` in the YAML and run the same command — the CLI scaffolds the three
   files and you refine them per
   [`references/additional-apis-workflow.md`](references/additional-apis-workflow.md). A
   hand-written file that duplicates what the CLI would produce drifts from the generator on the
   next regeneration (integrity hash mismatch → `.origin` conflict) and loses the benefit of the
   shared scaffold. **EXCEPTION**: hand-written protocol-flow composite handlers (e.g.,
   OAuth token issuance, JWKS) are legitimate — they implement non-aggregate concerns the CLI
   does not cover. They live under `@app/<bc>/<flow>/application/<flow>.handler.ts` and are
   registered in a dedicated `@api/<bc>/<flow>/<flow>.module.ts` (see "Composite handlers" section
   above + `catalyst-project-structure` "Hand-written protocol-flow endpoints" section).

8. **Dispatch logic duplicated across transports** (controller AND resolver both checking the same
   discriminator field to choose a sub-handler). → See "Composite handlers — Antipattern detection"
   section above. Extract a composite handler that owns the dispatch; reduce the transports to
   parsing.

## Naming Conventions

Names are determined by the YAML and enforced by the generator; follow them verbatim when writing
hand code that sits next to generated files.

| Artifact                  | Pattern                                    | Example                                      |
| ------------------------- | ------------------------------------------ | -------------------------------------------- |
| Handler class             | `[Pkg][Action][Entity]Handler`             | `IamCreateTagHandler`                        |
| Service class             | `[Pkg][Action][Entity]Service`             | `IamCreateTagService`                        |
| Resolver class            | `[Pkg][Action][Entity]Resolver`            | `IamCreateTagResolver`                       |
| Controller class          | `[Pkg][Action][Entity]Controller`          | `IamCreateTagController`                     |
| DTO class                 | `[Pkg][Action][Entity]Dto`                 | `IamCreateTagDto`                            |
| Event topic               | `<pkg>.<entity>.<past-tense>`              | `iam.tag.created`, `iam.tag.updated`         |
| File name (handler)       | `<pkg>-<action>-<entity>.handler.ts`       | `iam-create-tag.handler.ts`                  |
| File name (by-id variant) | `<pkg>-<action>-<entity>-by-id.handler.ts` | `iam-update-tag-by-id.handler.ts`            |
| Application folder        | `application/<action>/`                    | `application/create/`, `application/update/` |

For folder placement of every artifact see `catalyst-project-structure`.

## Import Order

Handlers follow the same order the rest of the backend does. The generator emits this order;
hand-written handlers match it to keep diffs clean on regeneration.

1. Node.js built-ins (rare in handlers — example: `import { readFile } from 'fs/promises'`).
2. NestJS (`@nestjs/common`, `@nestjs/event-emitter`, `@nestjs/graphql`).
3. External libraries (`@aurorajs.dev/core-back`, `@aurorajs.dev/core-common`, `uuid`, …).
4. Internal path aliases in order: `@api/*`, `@app/*`, `@infrastructure/*`, `@aurora/*`.
5. Relative imports.

## Cross-references

- **`catalyst-project-structure`** — where each artifact lives, module folder shape, `@api` vs
  `@app` boundaries. Read this before creating a new handler directory.
- **`catalyst-review-module`** — coherence audit of a whole module, including handlers. This
  composer skill and the review skill are complementary: compose here, audit there.
- **`catalyst-field-schema`** — the declarative data-access layer: `FieldSchema` type, 16 type
  handlers, the three decorators (`@ApplySchema` / `@Format` / `@Sanitize`), `Obj.diff` helper,
  `QueryStatement` shape, `Operator` enum, and the current `include` + `formatRecord` security gap
  with its workaround. Read this whenever you wire a decorator on `main()` or reach for a custom
  `where` / `include` clause.
- **`catalyst-nestjs-primitives`** — catalyst-authored primitives (`@Auth`,
  `AuthenticationJwtGuard`, `AuthorizationPermissionsGuard`), upstream catalog (`@Timezone`,
  `@Auditing`, `@CurrentAccount`, `@ContentLanguage`, `@TenantPolicy`, `@TenantConstraint`), the
  dual REST/GraphQL context pattern via `Api.getRequestFromExecutionContext()`, and the
  `dPermissions.all` convention. Read this when writing or reasoning about any guard, interceptor,
  pipe, filter, or custom decorator.
- **`catalyst-backend-testing`** — testing conventions for catalyst backend: `TestingModule` setup
  for handlers, the decorator-fires-in-TestingModule gotcha (`@ApplySchema`, `@EmitEvent`, `@Format`
  all run during `handler.main()` calls in tests), write- and read-handler templates, dual-context
  guard tests, e2e with supertest (REST + GraphQL), and the `dPermissions.all` seed pattern for
  `@Auth`-protected endpoint tests.
