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.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
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.
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.
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.
features/
├── EntryAfterCreateHandler/ ← ❌ technical name as feature directory
├── DocumentBeforeDeleteHandler/ ← ❌ technical name as feature directory
Rules
Feature directories describe business capability: syncToLingotek, cleanupOnDelete, notifySlack
❌ Not filtering event handlers by model/entity type
// WRONG — fires for ALL modelsasynchandle(event) {
awaitthis.service.doWork(event.payload.entry);
}
// CORRECT — filter by your modelasynchandle(event) {
if (event.payload.model.modelId !== MY_MODEL_ID) return;
awaitthis.service.doWork(event.payload.entry);
}
Domain events have handler abstractions with Interface + Event namespace
index.ts exports abstractions only — no features, no event classes, no implementations
All relative imports use .js extension
One class per file, one import per line
Core APIs
createAbstraction<T>(name: string)
Creates a typed DI token. The generic T is the interface that implementations must satisfy.
Import
import { createAbstraction } from "webiny/api"
Returns
Abstraction<T>
createFeature(def)
Creates a feature definition that the framework loads as an extension.
Import
import { createFeature } from "webiny/api"
def.name
Unique feature name (convention: "AppName/FeatureName")
def.register(container)
Called at startup with the DI Container instance
Key Rules
Abstractions first — any new business logic MUST be encapsulated in createAbstraction + createFeature. Never put logic directly in an EventHandler, GraphQL resolver, or CLI command.
Namespace convention — every abstraction exports namespace MyAbstraction { export type Interface = ...; } so consumers can type dependencies as MyAbstraction.Interface.
Name uniqueness — feature names must be globally unique; use "AppName/FeatureName" convention.
Constructor param order — dependencies array must match constructor parameter order exactly.
No process.env at runtime — deployed API services must NEVER read process.env. All configuration flows through BuildParams.