The universal createImplementation DI pattern and all injectable services. Use this skill when the developer is writing any Webiny extension and needs to understand dependency injection, constructor injection, how to access Logger/BuildParams/IdentityContext, how to inject CMS use-cases (list/get/create/update/delete entries), or how the dependencies array works. This is the connective tissue across all extension types -- API, Admin, CLI, and Infrastructure.
68
82%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Every Webiny extension type uses the same DI pattern: define a class implementing *.Interface, declare dependencies in the constructor, and export via *.createImplementation({ implementation, dependencies }). The DI container automatically provides the required services, ensures type safety, and validates at compile time. This pattern is the connective tissue across all extension types -- API, Admin, CLI, and Infrastructure.
import { SomeFactory } from "webiny/some/path";
import { Logger, BuildParams } from "webiny/api";
class MyImplementation implements SomeFactory.Interface {
constructor(
private logger: Logger.Interface,
private buildParams: BuildParams.Interface
) {}
execute(/* factory-specific params */) {
this.logger.info("Doing something...");
// buildParams.get() returns T | null — always account for null.
const value = this.buildParams.get<string>("MY_PARAM");
}
}
export default SomeFactory.createImplementation({
implementation: MyImplementation,
dependencies: [Logger, BuildParams]
});Key rules:
dependencies array.Feature.Interface.| Extension Type | Factory | Import Path |
|---|---|---|
| Content Models | ModelFactory | "webiny/api/cms/model" |
| GraphQL Schemas | GraphQLSchemaFactory | "webiny/api/graphql" |
| API Keys | ApiKeyFactory | "webiny/api/security" |
| CLI Commands | CliCommandFactory | "webiny/cli/command" |
| Pulumi Handlers | CorePulumi | "webiny/infra/core" |
Event handlers use the same
createImplementationpattern but are not injectable dependencies.
GraphQL schemas use the builder pattern. The execute method receives a builder and uses addTypeDefs and addResolver to define the schema. Resolver-level DI is declared per-resolver via dependencies in addResolver, resolved at request time from the request-scoped container.
import { GraphQLSchemaFactory } from "webiny/api/graphql";
import { IdentityContext } from "webiny/api/security";
class WhoAmISchema implements GraphQLSchemaFactory.Interface {
async execute(
builder: GraphQLSchemaFactory.SchemaBuilder
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
builder.addTypeDefs(/* GraphQL */ `
extend type Query {
whoAmI: String
}
`);
builder.addResolver({
path: "Query.whoAmI",
dependencies: [IdentityContext],
resolver: (identityContext: IdentityContext.Interface) => {
return () => {
const identity = identityContext.getIdentity();
return `Hello, ${identity.displayName}!`;
};
}
});
return builder;
}
}
export default GraphQLSchemaFactory.createImplementation({
implementation: WhoAmISchema,
dependencies: []
});Note: GraphQLSchemaFactory implementations typically have dependencies: [] because DI happens at the resolver level via addResolver({ dependencies }), not at the class constructor level.
import { Ui } from "webiny/cli";
import { CliCommandFactory } from "webiny/cli/command";
class MyCommandImpl implements CliCommandFactory.Interface<{ name: string }> {
constructor(private ui: Ui.Interface) {}
execute(): CliCommandFactory.CommandDefinition<{ name: string }> {
return {
name: "greet",
description: "Greet someone",
params: [{ name: "name", description: "Name", type: "string" }],
handler: async params => {
this.ui.success(`Hello, ${params.name}!`);
}
};
}
}
export default CliCommandFactory.createImplementation({
implementation: MyCommandImpl,
dependencies: [Ui]
});import { Ui } from "webiny/infra";
import { CorePulumi } from "webiny/infra/core";
class MyPulumiImpl implements CorePulumi.Interface {
constructor(private ui: Ui.Interface) {}
execute(app: any) {
this.ui.info("Deploying with environment:", app.env);
}
}
export default CorePulumi.createImplementation({
implementation: MyPulumiImpl,
dependencies: [Ui]
});The dependencies array supports three forms per entry:
| Form | Meaning |
|---|---|
Abstraction | Single required dependency (shorthand) |
[Abstraction, { optional: true }] | Single optional dependency — injects undefined if not registered |
[Abstraction, { multiple: true }] | Multi-injection — injects all registered implementations as T[] |
[Abstraction, { multiple: true, optional: true }] | Multi-injection, optional — injects undefined if none registered (vs empty [] with just multiple) |
{ multiple: true })Use when a class needs all registered implementations of an abstraction. The container calls resolveAll() internally and injects the results as an array.
Abstraction:
interface IPageType {
name: string;
label: string;
modify(form: IFormModel): void;
}
export const PageType = createAbstraction<IPageType>("PageType");
export namespace PageType {
export type Interface = IPageType;
}Multiple implementations registered separately:
// StaticPageType.ts
class StaticPageTypeImpl implements PageType.Interface {
name = "static";
label = "Static Page";
modify(form: IFormModel) {
/* no-op — base form is sufficient */
}
}
export const StaticPageType = PageType.createImplementation({
implementation: StaticPageTypeImpl,
dependencies: []
});
// ProductPageType.ts (in another package/extension)
class ProductPageTypeImpl implements PageType.Interface {
name = "product";
label = "Product Page";
modify(form: IFormModel) {
form.fields(fields => ({
product: fields.select().label("Product").required("Product is required")
}));
form.field("title").disabled(true);
form.field("path").disabled(true);
}
}
export const ProductPageType = PageType.createImplementation({
implementation: ProductPageTypeImpl,
dependencies: []
});Consumer injects the array:
class CreatePagePresenterImpl implements CreatePagePresenter.Interface {
constructor(
private factory: FormModelFactory.Interface,
private pageTypes: PageType.Interface[],
private modifiers: CreatePageFormModifier.Interface[]
) {}
}
export const CreatePagePresenter = PresenterAbstraction.createImplementation({
implementation: CreatePagePresenterImpl,
dependencies: [
FormModelFactory,
[PageType, { multiple: true }],
[CreatePageFormModifier, { multiple: true }]
]
});Registration — each implementation is a separate container.register() call:
export const CreatePageFeature = createFeature({
name: "CreatePage",
register(container) {
container.register(StaticPageType); // first PageType impl
container.register(ProductPageType); // second PageType impl
container.register(CreatePagePresenter);
}
});When CreatePagePresenter is resolved, pageTypes receives [StaticPageTypeImpl, ProductPageTypeImpl] in registration order.
{ optional: true })Use when a dependency may not be registered. The container injects undefined instead of throwing.
class MyPresenterImpl {
constructor(
private required: RequiredService.Interface,
private analytics: AnalyticsService.Interface | undefined
) {}
}
export const MyPresenter = Abstraction.createImplementation({
implementation: MyPresenterImpl,
dependencies: [RequiredService, [AnalyticsService, { optional: true }]]
});| Method | Description |
|---|---|
container.register(Impl) | Register a class implementation. Returns RegistrationBuilder with .inSingletonScope(). Multiple registrations of the same abstraction accumulate — resolve() returns the last, resolveAll() returns all. |
container.registerInstance(Abstraction, instance) | Register a pre-built instance (no constructor resolution). |
container.registerFactory(Abstraction, factory) | Register a factory function. Called on every resolve(). |
container.registerDecorator(Decorator) | Register a decorator that wraps resolved instances. Applied in registration order. |
container.registerComposite(Composite) | Register a composite that aggregates all implementations behind a single resolve(). |
| Method | Description |
|---|---|
container.resolve(Abstraction) | Resolve single instance (last registered wins). Throws if not registered. |
container.resolveAll(Abstraction) | Resolve all registered implementations as T[]. Returns empty array if none. |
container.createChildContainer() | Create a child container that inherits parent registrations. |
resolve()..inSingletonScope()): Cached after first resolution, one instance per container.Convention: Use cases = transient. Repositories, gateways, services, registries = singleton.
Decorators wrap resolved instances. The decoratee is always the last constructor parameter. The dependencies array does NOT include the decoratee.
class LoggingServiceDecorator implements MyService.Interface {
constructor(
private logger: Logger.Interface,
private decoratee: MyService.Interface // LAST param — injected automatically
) {}
execute() {
this.logger.info("Before");
this.decoratee.execute();
}
}
export const MyServiceLoggingDecorator = MyService.createDecorator({
decorator: LoggingServiceDecorator,
dependencies: [Logger] // decoratee is NOT listed
});
// Registration:
container.registerDecorator(MyServiceLoggingDecorator);Composites aggregate multiple implementations behind a single resolve() call. Created via Abstraction.createComposite():
class AllValidatorsComposite implements Validator.Interface {
constructor(private validators: Validator.Interface[]) {}
validate(input: unknown) {
for (const v of this.validators) v.validate(input);
}
}
export const ValidatorComposite = Validator.createComposite({
implementation: AllValidatorsComposite,
dependencies: [[Validator, { multiple: true }]]
});
// Registration:
container.registerComposite(ValidatorComposite);Feature.Interface for constructor parameter types.dependencies array order must match the constructor parameter order.abstractions.ts file in the feature folder to see available methods.dependencies: [].BuildParams.get<T>(name) returns T | null — always type the receiving property/variable as nullable (e.g. string | null) and handle the null case.Extension.tsx, not in webiny.config.tsx. Expose required params as React props on the extension component so the consumer decides where values come from (see webiny-full-stack-architect skill for the full pattern).T[] and use [Abstraction, { multiple: true }] in the dependencies array.container.register() call — they accumulate.webiny-custom-graphql-api -- DI in GraphQL schema extensionswebiny-cli-extensions -- DI in CLI command extensionswebiny-full-stack-architect -- Full-stack extension skeleton and registration patternwebiny-api-architect -- API-side architecture using DIwebiny-admin-architect -- Admin-side architecture using DI484553b
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.