webiny-use-case-pattern

UseCase implementation pattern — DI, Result handling, error types, decorators, CMS repositories, entry mappers, and schema-based permissions. Use this skill to implement, inject, override, or decorate any Webiny UseCase, or to build repositories that persist data via CMS.

7,955 stars

Best use case

webiny-use-case-pattern is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

UseCase implementation pattern — DI, Result handling, error types, decorators, CMS repositories, entry mappers, and schema-based permissions. Use this skill to implement, inject, override, or decorate any Webiny UseCase, or to build repositories that persist data via CMS.

Teams using webiny-use-case-pattern should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/use-case-pattern/SKILL.md --create-dirs "https://raw.githubusercontent.com/webiny/webiny-js/main/skills/user-skills/api/use-case-pattern/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/use-case-pattern/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How webiny-use-case-pattern Compares

Feature / Agentwebiny-use-case-patternStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

UseCase implementation pattern — DI, Result handling, error types, decorators, CMS repositories, entry mappers, and schema-based permissions. Use this skill to implement, inject, override, or decorate any Webiny UseCase, or to build repositories that persist data via CMS.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

Related Guides

SKILL.md Source

# UseCase Pattern

## What It Is

A **UseCase** is a single-method orchestrator that encapsulates one business operation (e.g., `CreateTenantUseCase`, `PublishEntryUseCase`). Each UseCase is a DI abstraction with an `execute` method that returns `Result<T, E>`.

## Interface Shape

```ts
interface SomeUseCase.Interface {
    execute(input: Input): Promise<Result<ReturnType, ErrorType>>;
}
```

- **Input** — a typed object specific to the use case
- **Result** — always returns `Result<T, E>` from `@webiny/feature/api`
- **Error** — extends `BaseError` with a unique `code`

## How to Use a UseCase

UseCases are injected as dependencies into EventHandlers, other UseCases, or GraphQL resolvers via DI.

```ts
import { SomeUseCase } from "webiny/api/<category>";
import { SomeEventHandler } 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({ /* input */ });

        if (result.isFail()) {
            console.error(result.error.message);
            return;
        }

        const value = result.value;
        // ... use value
    }
}

export default SomeEventHandler.createImplementation({
    implementation: MyHandler,
    dependencies: [SomeUseCase]
});
```

## How to Override a UseCase

To replace the default implementation, register your own:

```ts
import { SomeUseCase } from "webiny/api/<category>";

class CustomImplementation implements SomeUseCase.Interface {
    async execute(input) {
        // Custom logic
        return Result.ok(/* ... */);
    }
}

export default SomeUseCase.createImplementation({
    implementation: CustomImplementation,
    dependencies: []
});
```

## Registration

**YOU MUST include the full file path with the `.ts` extension in the `src` prop.** For example, use `src={"@/extensions/my-extension.ts"}`, NOT `src={"@/extensions/my-extension"}`. 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
// In your app's configuration
<Api.Extension src={"@/extensions/my-extension.ts"} />
```

Deploy with: `yarn webiny deploy api --env=dev`

---

## Error Handling Pattern

### Domain-Specific Errors

Every feature defines errors extending `BaseError`. Never use generic `Error` for validation or business rule failures.

```ts
// domain/errors.ts
import { BaseError } from "@webiny/feature/api";

export class EntityNotFoundError extends BaseError {
    override readonly code = "Entity/NotFound" as const;
    constructor(id: string) {
        super({ message: `Entity with id "${id}" was not found!` });
    }
}

export class EntityPersistenceError extends BaseError<{ error: Error }> {
    override readonly code = "Entity/Persist" as const;
    constructor(error: Error) {
        super({ message: error.message, data: { error } });
    }
}

export class EntityValidationError extends BaseError<{ message: string }> {
    override readonly code = "Entity/Validation" as const;
    constructor(message: string) {
        super({ message, data: { message } });
    }
}
```

### Typed Error Unions in Abstractions

Define an `IErrors` interface mapping error names to types, then create a union via `[keyof IErrors]`:

```ts
// features/createEntity/abstractions.ts
import { createAbstraction, Result } from "@webiny/feature/api";
import { NotAuthorizedError } from "@webiny/api-core/features/security/shared/errors.js";
import { EntityPersistenceError, EntityModelNotFoundError, EntityCreationError } from "~/api/domain/errors.js";

// REPOSITORY errors
export interface ICreateEntityRepositoryErrors {
    persistence: EntityPersistenceError;
    modelNotFound: EntityModelNotFoundError;
    creation: EntityCreationError;
}

type RepositoryError = ICreateEntityRepositoryErrors[keyof ICreateEntityRepositoryErrors];

export interface ICreateEntityRepository {
    execute(entity: Entity): Promise<Result<Entity, RepositoryError>>;
}

export const CreateEntityRepository = createAbstraction<ICreateEntityRepository>(
    "MyExt/CreateEntityRepository"
);

export namespace CreateEntityRepository {
    export type Interface = ICreateEntityRepository;
    export type Error = RepositoryError;
    export type Return = Promise<Result<Entity, RepositoryError>>;
}

// USE CASE errors — superset of repository errors
export interface ICreateEntityUseCaseErrors {
    persistence: EntityPersistenceError;
    modelNotFound: EntityModelNotFoundError;
    creation: EntityCreationError;
    notAuthorized: NotAuthorizedError;
}

type UseCaseError = ICreateEntityUseCaseErrors[keyof ICreateEntityUseCaseErrors];

export interface ICreateEntityUseCase {
    execute(input: CreateEntityInput): Promise<Result<Entity, UseCaseError>>;
}

export const CreateEntityUseCase = createAbstraction<ICreateEntityUseCase>(
    "MyExt/CreateEntityUseCase"
);

export namespace CreateEntityUseCase {
    export type Interface = ICreateEntityUseCase;
    export type Input = CreateEntityInput;
    export type Error = UseCaseError;
    export type Return = Promise<Result<Entity, UseCaseError>>;
}
```

### Result Pattern

```ts
// Success
return Result.ok(value);

// Failure
return Result.fail(new EntityNotFoundError(id));

// Check result
if (result.isFail()) {
    return Result.fail(result.error);
}

// Access value
const value = result.value;
```

Never use `result.isError()`, `result.getError()`, or `result.getValue()` — these do not exist.

---

## UseCase Implementation

```ts
// features/createEntity/CreateEntityUseCase.ts
import { CreateEntityUseCase as UseCaseAbstraction, CreateEntityRepository } from "./abstractions.js";
import { Result } from "@webiny/feature/api";
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
import { NotAuthorizedError } from "@webiny/api-core/features/security/shared/errors.js";
import { Entity } from "~/shared/Entity.js";
import { EntityId } from "~/api/domain/EntityId.js";

class CreateEntityUseCase implements UseCaseAbstraction.Interface {
    constructor(
        private identityContext: IdentityContext.Interface,
        private repository: CreateEntityRepository.Interface
    ) {}

    async execute(input: UseCaseAbstraction.Input): UseCaseAbstraction.Return {
        if (!this.identityContext.getPermission("mypackage.entity")) {
            return Result.fail(new NotAuthorizedError({ message: "Not authorized to create entities!" }));
        }

        const entity = Entity.from({
            id: EntityId.from(input.id),
            values: { name: input.name, status: "disabled" }
        });

        const result = await this.repository.execute(entity);
        if (result.isFail()) {
            return Result.fail(result.error);
        }

        return Result.ok(result.value);
    }
}

export default UseCaseAbstraction.createImplementation({
    implementation: CreateEntityUseCase,
    dependencies: [IdentityContext, CreateEntityRepository]
});
```

**Rules:**
- Class implements `UseCaseAbstraction.Interface`
- Constructor params typed with `.Interface` from their abstractions
- Return type uses `UseCaseAbstraction.Return`
- `dependencies` array matches constructor parameter order exactly
- Export as `default`

---

## CMS Repository Pattern

Repositories use CMS use cases to persist data. Always resolve the CMS model first.

```ts
// features/createEntity/CreateEntityRepository.ts
import { Entity } from "~/shared/Entity.js";
import { EntityCreationError, EntityModelNotFoundError } from "~/api/domain/errors.js";
import { CreateEntityRepository as RepositoryAbstraction } from "./abstractions.js";
import { Result } from "@webiny/feature/api";
import { CreateEntryUseCase } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { GetModelUseCase } from "@webiny/api-headless-cms/exports/api/cms/model";
import { ENTITY_MODEL_ID } from "~/shared/constants.js";

class CreateEntityRepository implements RepositoryAbstraction.Interface {
    constructor(
        private getModelUseCase: GetModelUseCase.Interface,
        private createEntryUseCase: CreateEntryUseCase.Interface
    ) {}

    async execute(entity: Entity): RepositoryAbstraction.Return {
        const modelResult = await this.getModelUseCase.execute(ENTITY_MODEL_ID);
        if (modelResult.isFail()) {
            return Result.fail(new EntityModelNotFoundError());
        }

        const createResult = await this.createEntryUseCase.execute(modelResult.value, {
            id: entity.id,
            values: {
                name: entity.values.name,
                status: entity.values.status
            }
        });

        if (createResult.isFail()) {
            return Result.fail(new EntityCreationError(createResult.error));
        }

        return Result.ok(entity);
    }
}

export default RepositoryAbstraction.createImplementation({
    implementation: CreateEntityRepository,
    dependencies: [GetModelUseCase, CreateEntryUseCase]
});
```

### Common CMS Use Cases for Repositories

```ts
import { CreateEntryUseCase } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { GetEntryByIdUseCase } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { GetEntryUseCase } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { UpdateEntryUseCase } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { ListLatestEntriesUseCase } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { EntryId } from "@webiny/api-headless-cms/exports/api/cms/entry.js";
import { GetModelUseCase } from "@webiny/api-headless-cms/exports/api/cms/model";
import { ListModelsUseCase } from "@webiny/api-headless-cms/exports/api/cms/model.js";
```

**Rules:**
- Always resolve the CMS model first via `GetModelUseCase`
- Wrap CMS errors in domain-specific errors
- Register repositories in **singleton scope**
- Export as `default`

---

## Entry-to-Entity Mapper

When repositories return CMS entries, use a mapper to convert to domain types:

```ts
// features/shared/EntryToEntityMapper.ts
import { Entity as EntityClass } from "~/shared/Entity.js";
import type { Entity, EntityDto, EntityValues } from "~/shared/Entity.js";

export class EntryToEntityMapper {
    static toEntity(entry: { entryId: string; values: EntityValues }): Entity {
        return EntityClass.from({
            id: entry.entryId,
            values: entry.values
        });
    }
}
```

- Static methods only — no instance state
- Used by repositories, not by use cases directly
- Handle null/undefined values with defaults where appropriate

---

## UseCase Decorators

Decorators add cross-cutting concerns (authorization, logging, validation) without modifying the core use case.

```ts
// features/getEntityById/decorators/GetEntityByIdWithAuthorization.ts
import { GetEntityByIdUseCase } from "../abstractions.js";
import { Result } from "@webiny/feature/api";
import { IdentityContext } from "@webiny/api-core/exports/api/security.js";
import { NotAuthorizedError } from "@webiny/api-core/features/security/shared/errors.js";

class GetEntityByIdWithAuthorizationImpl implements GetEntityByIdUseCase.Interface {
    constructor(
        private identityContext: IdentityContext.Interface,
        private decoratee: GetEntityByIdUseCase.Interface  // decoratee is LAST
    ) {}

    async execute(id: string): GetEntityByIdUseCase.Return {
        if (!this.identityContext.getPermission("mypackage.entity")) {
            return Result.fail(new NotAuthorizedError());
        }
        return this.decoratee.execute(id);
    }
}

export const GetEntityByIdWithAuthorization = GetEntityByIdUseCase.createDecorator({
    decorator: GetEntityByIdWithAuthorizationImpl,
    dependencies: [IdentityContext]  // does NOT include decoratee
});
```

### Registering a Decorator

```ts
// features/getEntityById/feature.ts
import { createFeature } from "@webiny/feature/api";
import GetEntityByIdUseCase from "./GetEntityByIdUseCase.js";
import GetEntityByIdRepository from "./GetEntityByIdRepository.js";
import { GetEntityByIdWithAuthorization } from "./decorators/GetEntityByIdWithAuthorization.js";

export const GetEntityByIdFeature = createFeature({
    name: "GetEntityById",
    register(container) {
        container.register(GetEntityByIdUseCase);
        container.register(GetEntityByIdRepository).inSingletonScope();
        container.registerDecorator(GetEntityByIdWithAuthorization);
    }
});
```

**Rules:**
- Implements the same interface as the use case it decorates
- Constructor: extra dependencies first, `decoratee` **last**
- Use `UseCaseAbstraction.createDecorator(...)` — the `dependencies` array does NOT include the decoratee
- Register with `container.registerDecorator()`, not `container.register()`
- Can modify input before delegating, output after, or short-circuit with an error

---

## Schema-Based Permissions

For implementing authorization in use cases, see the **webiny-api-permissions** skill. It covers:
- Permission schema definition with `createPermissions`
- All permission methods (`canRead`, `canEdit`, `canDelete`, `canPublish`, `onlyOwnRecords`, etc.)
- Use case patterns for every CRUD operation (get, list, update, delete, publish)
- Own-record scoping and item-level ownership checks
- Testing patterns and permission object shapes

---

## Resolving Types (MANDATORY)

**Before writing any code that calls a UseCase or accesses its return types, you MUST read the source file listed in the catalog's `Source` field to verify the exact method signatures, input parameters, return types, and error types. Do not assume or guess property names from memory.**

1. Read the `abstractions.ts` file from the catalog `Source` path
2. If the interface references domain types, follow the import and read that type declaration
3. Only use properties and method signatures confirmed in the source

## Key Rules

- Always check `result.isFail()` before accessing `.value` or `.error`
- 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, anti-patterns
- **webiny-api-permissions** — Schema-based permissions, CRUD authorization patterns, testing
- **webiny-event-handler-pattern** — EventHandler lifecycle, domain event publishing
- **webiny-custom-graphql-api** — GraphQL schema creation with UseCase DI
- **webiny-dependency-injection** — Injectable services catalog

Related Skills

webiny-v5-to-v6-migration

7955
from webiny/webiny-js

Migration patterns for converting v5 Webiny code to v6 architecture. Use this skill when migrating existing v5 plugins to v6 features, converting context plugins to DI services, adapting v5 event subscriptions to v6 EventHandlers, or understanding how v5 patterns translate to v6. Targeted at AI agents performing migrations.

webiny-api-permissions

7955
from webiny/webiny-js

Schema-based permission system for API features. Use this skill when implementing authorization in use cases, defining permission schemas with createPermissionSchema, creating injectable permissions via createPermissionsAbstraction/createPermissionsFeature, checking read/write/delete/publish permissions, handling own-record scoping, or testing permission scenarios. Covers the full pattern from schema definition to use case integration to test matrices.

webiny-admin-permissions

7955
from webiny/webiny-js

Admin-side permission UI registration and DI-backed permission checking. Use this skill when adding permission controls to the admin UI — schema-based auto-generated forms, injectable permissions via createPermissionsAbstraction/ createPermissionsFeature, typed hooks (createUsePermissions), the HasPermission component (createHasPermission), and the Security.Permissions component props. Covers both simple apps and complex multi-entity permission schemas.

webiny-sdk

7955
from webiny/webiny-js

Using @webiny/sdk to read and write CMS data from external applications. Use this skill when the developer is building a Next.js, Vue, Node.js, or any external app that needs to fetch or write content to Webiny, set up the SDK, use the Result pattern, list/get/create/update/publish entries, filter and sort queries, use TypeScript generics for type safety, work with the File Manager, or create API keys programmatically. Covers read vs preview mode, the `values` wrapper requirement, correct method names, and the `fields` required parameter.

webiny-project-structure

7955
from webiny/webiny-js

Webiny project layout, webiny.config.tsx anatomy, and extension registration. Use this skill when the developer asks about folder structure, where custom code goes, how to register extensions, what webiny.config.tsx does, or how the project is organized. Also use when they need to understand the relationship between extensions/, webiny.config.tsx, and the different extension types (Api, Admin, Infra, CLI).

webiny-local-development

7955
from webiny/webiny-js

Deploying, developing locally, managing environments, and debugging Webiny projects. Use this skill when the developer asks about deployment commands (deploy, destroy, info), local development with watch mode (API or Admin), the Local Lambda Development system, environment management (long-lived vs short-lived, production vs dev modes), build parameters, state files, debugging API/Admin/Infrastructure errors, or the redeploy-after-watch requirement.

webiny-infrastructure-extensions

7955
from webiny/webiny-js

Modifying AWS infrastructure using Pulumi handlers and declarative Infra components. Use this skill when the developer wants to customize AWS infrastructure, add Pulumi handlers, configure OpenSearch, VPC, resource tags, regions, custom domains, blue-green deployments, environment-conditional config, or manage production vs development infrastructure modes. Covers CorePulumi.Interface, all <Infra.*> declarative components, and <Infra.Env.Is>.

webiny-infra-catalog

7955
from webiny/webiny-js

Infrastructure — 33 abstractions. Infrastructure extensions.

webiny-extensions-catalog

7955
from webiny/webiny-js

extensions — 5 abstractions.

webiny-cli-command-catalog

7955
from webiny/webiny-js

cli/command — 1 abstractions.

webiny-cli-catalog

7955
from webiny/webiny-js

cli — 2 abstractions.

webiny-api-tenant-manager-catalog

7955
from webiny/webiny-js

API — Tenant Manager — 2 abstractions. Tenant management event handlers and use cases.