Skip to main content

webiny-api-architect

The hub skill for all API/backend architecture in Webiny. Covers architecture overview, Services vs UseCases, feature naming and organization, feature structure templates, DI decision tree, anti-patterns, createFeature, createAbstraction, container registration, domain errors, entity patterns, naming conventions, scoping rules, and code conventions. Use this skill for ANY backend API work — it references sub-skills for deep implementation details.

ソース情報

リポジトリ
webiny/webiny-js
ソースの最終更新活動
2026年9月9日 11:38
検出された SKILL.md の言語
英語
スター
8,049
フォーク
682

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
webiny-api-architect
description
The hub skill for all API/backend architecture in Webiny. Covers architecture overview, Services vs UseCases, feature naming and organization, feature structure templates, DI decision tree, anti-patterns, createFeature, createAbstraction, container registration, domain errors, entity patterns, naming conventions, scoping rules, and code conventions. Use this skill for ANY backend API work — it references sub-skills for deep implementation details.
# API Architecture Patterns ## TL;DR API extensions use `createFeature` to register features into the DI container. Each feature is a vertical slice with abstractions, implementations, and a `feature.ts` registration file. The key abstractions are **Services** (multi-method, singleton) and **UseCases** (single-method orchestrators, transient). Repositories handle persistence via CMS. Features are named by **business capability**, files inside by **technical responsibility**. ## Working Context This skill applies to both **extension developers** (working in `extensions/`) and **core developers** (working in `packages/`). The architecture patterns are identical — only imports and registration differ. | | Extensions (`extensions/`) | Core (`packages/`) | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | **Imports** | `webiny/api`, `webiny/api/cms/model`, etc. | `@webiny/feature/api`, `@webiny/api-headless-cms/...`, etc. | | **Catalog paths** | Use the `Import:` path | Use the `Source:` path | | **Entry point** | `export default createFeature(...)` in a file targeted by `<Api.Extension src={...}>` | `createFeature` registered by the package initializer | | **GraphQL schemas** | `export default GraphQLSchemaFactory.createImplementation(...)` — registered via `container.register()` inside the entry point's `createFeature` | Same pattern, but imported from `@webiny/handler-graphql` | Detect which context you're in by checking the file path: `extensions/` → extension mode, `packages/` → core mode. ## Architecture Overview ``` Extension (root) ── registers ──> Features + GraphQL Schemas + Models Feature ── registers ──> UseCase | Service | EventHandler + Repository UseCase ── depends on ──> Service | Repository (+ EventPublisher) Repository ── depends on ──> CMS Use Cases (GetModel, CreateEntry, etc.) Service ── depends on ──> external APIs, other Services ``` - **Extension**: Top-level entry point. Registers all features, GraphQL schemas, and CMS models. - **Feature**: A vertical slice. Registers its use cases, services, repositories, and event handlers. - **UseCase**: Single-method orchestrator (`execute()`). Coordinates services, repositories, and events. Transient scope. - **Service**: Multi-method abstraction for external API calls or cohesive domain logic. Singleton scope. - **Repository**: Persistence layer using CMS as storage. Singleton scope. - **EventHandler**: Thin orchestrator reacting to domain events. Delegates to services/use cases. - **GraphQL Schema**: Defines types, inputs, queries, and mutations. Resolvers delegate to use cases. - **CMS Model**: Defines the data schema stored in headless CMS. --- ## Services vs UseCases ### Services Multi-method abstractions for **external API calls** or **cohesive domain logic**. A service groups related operations that belong together. ```ts // abstractions.ts export interface ILingotekService { translate(documentId: string, targetLocale: string): Promise<Result<void, Error>>; getTranslationStatus(documentId: string): Promise<Result<TranslationStatus, Error>>; deleteProject(projectId: string): Promise<Result<void, Error>>; } export const LingotekService = createAbstraction<ILingotekService>("MyExt/LingotekService"); export namespace LingotekService { export type Interface = ILingotekService; } ``` - Registered in **singleton scope** (`.inSingletonScope()`) - Located in: `features/{serviceName}/` or `features/services/{serviceName}/` - One service per external system or cohesive domain area - **If async bootstrap is needed** (loading settings from CMS, fetching remote config): use the **ServiceProvider pattern** — a provider abstraction with `async getService()` that lazily initializes and caches the service. Consumers inject the provider, not the service directly. See the ServiceProvider section below. ### UseCases Single-method orchestrators with an `execute()` method. They coordinate services, repositories, and events. ```ts export interface ISyncProjectUseCase { execute(input: SyncProjectInput): Promise<Result<Project, SyncProjectError>>; } ``` - Registered in **transient scope** (default) - Located in: `features/{ActionEntity}/` - One use case per business operation ### When to Create a UseCase - GraphQL mutations need the same logic as event handlers - Need to coordinate multiple services or repositories - Business logic must be reusable across entry points (GraphQL, events, CLI) ### When NOT to Create a UseCase - Simple event handler that calls one service method — inject the service directly - Simple read queries — inject the service or repository directly into the GraphQL resolver - Logic that only exists in one place and is unlikely to be reused ### ServiceProvider Pattern (Async Bootstrap) When a service requires async initialization (loading CMS settings, fetching remote config, API tokens), use a **ServiceProvider** — a provider abstraction with `async getService()` that lazily creates and caches the service. Both the provider and the service are part of the same feature. The provider is the primary abstraction exported from the feature. The service itself is not registered in the DI container. ```ts // abstractions.ts export interface ILingotekServiceProvider { getService(): Promise<ILingotekService>; } export const LingotekServiceProvider = createAbstraction<ILingotekServiceProvider>( "MyExt/LingotekServiceProvider" ); export namespace LingotekServiceProvider { export type Interface = ILingotekServiceProvider; } ``` ```ts // LingotekServiceProvider.ts class LingotekServiceProviderImpl implements ProviderAbstraction.Interface { private service: ILingotekService | undefined; constructor(private getSettings: GetSettingsUseCase.Interface) {} async getService(): Promise<ILingotekService> { if (!this.service) { const result = await this.getSettings.execute(); const settings = result.isOk() ? result.value : defaultSettings; this.service = new LingotekService(settings); } return this.service; } } ``` - Register the **provider** in singleton scope (it caches the service) - The service itself is NOT registered in DI — it's created by the provider - Consumers call `await provider.getService()` before using the service - Use cases and handlers inject `LingotekServiceProvider`, not `LingotekService` --- ## Feature Naming Philosophy Features use a **two-level naming convention**: - **Feature directory** = business capability (what it does for the business) - **Files inside** = technical responsibility (what each file handles) This makes features **discoverable by what they DO**, and once inside a feature folder, you see the technical components clearly. ### Good ``` features/ ├── syncToLingotek/ ← business capability │ ├── abstractions.ts │ ├── SyncProjectUseCase.ts ← technical responsibility │ ├── EntryAfterCreateHandler.ts ← technical responsibility (fine as filename!) │ ├── EntryAfterUpdateHandler.ts │ └── feature.ts ├── cleanupLingotekDocument/ │ ├── EntryBeforeDeleteHandler.ts │ └── feature.ts ``` ### Bad ``` features/ ├── EntryAfterCreateHandler/ ← ❌ technical name as feature directory ├── DocumentBeforeDeleteHandler/ ← ❌ technical name as feature directory ``` ### Rules - Feature directories describe **business capability**: `syncToLingotek`, `cleanupOnDelete`, `notifySlack` - Files inside describe **technical responsibility**: `EntryAfterCreateHandler.ts`, `SyncProjectUseCase.ts` - Event handlers ARE features — they live in `features/`, never in a separate `handlers/` directory --- ## Feature Structure Templates ### Simple Event Handler Feature When: handler calls a service or use case, no new abstractions needed. ``` features/cleanupOnDelete/ ├── CleanupOnDeleteHandler.ts # Implements an existing EventHandler abstraction └── feature.ts # Registers the handler ``` ### Complex Feature with UseCases When: logic is reused by GraphQL + event handlers, or coordinates multiple services. ``` features/syncProjectToLingotek/ ├── abstractions.ts # UseCase + error types for this feature ├── CreateProjectUseCase.ts ├── UpdateProjectUseCase.ts ├── DeleteProjectUseCase.ts ├── EntryAfterCreateHandler.ts # Thin handler → delegates to CreateProjectUseCase ├── EntryAfterUpdateHandler.ts # Thin handler → delegates to UpdateProjectUseCase ├── EntryAfterDeleteHandler.ts # Thin handler → delegates to DeleteProjectUseCase └── feature.ts # Registers everything ``` ### Service Feature When: reusable multi-method service for an external API or domain area. ``` features/lingotekService/ ├── abstractions.ts # Service interface (multi-method) ├── LingotekService.ts # Implementation └── feature.ts # Registers in singleton scope ``` --- ## DI Decision Tree ### What to inject based on what you're building | You're building a... | It needs to... | Inject | | -------------------- | -------------------------- | ------------------------------------------ | | **Event Handler** | Call external API | Service | | **Event Handler** | Orchestrate CMS + external | UseCase | | **Event Handler** | Just log/validate | Logger (or nothing) | | **GraphQL Resolver** | Simple read | Service or Repository directly | | **GraphQL Resolver** | Complex mutation | UseCase | | **GraphQL Resolver** | Check permissions | IdentityContext or Permissions abstraction | | **UseCase** | Call external API | Service | | **UseCase** | Persist/read data | Repository | | **UseCase** | Publish domain events | EventPublisher | | **UseCase** | Check permissions | IdentityContext or Permissions abstraction | | **Repository** | Access CMS | GetModelUseCase, CreateEntryUseCase, etc. | --- ## Anti-Patterns ### ❌ Creating one abstraction per operation instead of a multi-method Service ```ts // WRONG — separate abstractions for related operations export const DeleteDocumentService = createAbstraction(...) export const CreateDocumentService = createAbstraction(...) export const UpdateDocumentService = createAbstraction(...) // CORRECT — one multi-method Service export interface IDocumentService { create(input: CreateInput): Promise<Result<Doc, Error>>; update(id: string, input: UpdateInput): Promise<Result<Doc, Error>>; delete(id: string): Promise<Result<void, Error>>; } export const DocumentService = createAbstraction<IDocumentService>("MyExt/DocumentService"); ``` ### ❌ Naming features by technical implementation ``` features/DocumentBeforeDeleteHandler/ ← WRONG: technical name features/cleanupLingotekDocument/ ← CORRECT: business capability ``` ### ❌ Assuming builders exist for factories ```ts // WRONG — no builder pattern exists builder.role({ ... }).permissions([...]) // CORRECT — factories return plain objects async execute(): Promise<CodeRole[]> { return [{ name: "Admin", slug: "admin", description: "...", permissions: [...] }]; } ``` ### ❌ Separate handlers/ directory ``` api/handlers/MyHandler.ts ← WRONG: handlers are features features/myFeature/MyHandler.ts ← CORRECT: handler lives inside its feature ``` ### ❌ Using generic Error instead of domain-specific errors ```ts // WRONG throw new Error("Not found"); // CORRECT return Result.fail(new EntityNotFoundError(id)); ``` ### ❌ Not filtering event handlers by model/entity type ```ts // WRONG — fires for ALL models async handle(event) { await this.service.doWork(event.payload.entry); } // CORRECT — filter by your model async handle(event) { if (event.payload.model.modelId !== MY_MODEL_ID) return; await this.service.doWork(event.payload.entry); } ``` --- ## API Directory Structure ``` api/ ├── Extension.ts # API entry point (createFeature, registers everything) ├── domain/ │ ├── errors.ts # Domain-specific errors (extend BaseError) │ ├── EntityId.ts # Value object for entity IDs │ ├── EntityModel.ts # CMS model definition (ModelFactory) │ └── EntityModelExtension.ts # Abstraction for extending the model ├── features/ │ ├── createEntity/ # Feature: business capability │ │ ├── abstractions.ts # UseCase + Repository abstractions + error types │ │ ├── feature.ts # DI registration │ │ ├── CreateEntityUseCase.ts │ │ └── CreateEntityRepository.ts │ ├── lingotekService/ # Service feature │ │ ├── abstractions.ts │ │ ├── LingotekService.ts │ │ └── feature.ts │ └── syncToLingotek/ # Event handler feature │ ├── EntryAfterCreateHandler.ts │ └── feature.ts └── graphql/ ├── CreateEntitySchema.ts └── GetEntitySchema.ts ``` ## API Extension Entry Point ```ts // src/api/Extension.ts import { createFeature } from "webiny/api"; import EntityModel from "./domain/EntityModel.js"; import CreateEntitySchema from "./graphql/CreateEntitySchema.js"; import { CreateEntityFeature } from "./features/createEntity/feature.js"; import { LingotekServiceFeature } from "./features/lingotekService/feature.js"; import { SyncToLingotekFeature } from "./features/syncToLingotek/feature.js"; export const Extension = createFeature({ name: "MyExtension", register(container) { // CMS model (register first) container.register(EntityModel); // GraphQL schemas container.register(CreateEntitySchema); // Features (use Feature.register, NOT container.register) CreateEntityFeature.register(container);
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る