Add custom GraphQL queries and mutations using GraphQLSchemaFactory. Implement GraphQLSchemaFactory.Interface, use the schema builder to add type definitions and resolvers (with per-resolver DI), and export with GraphQLSchemaFactory.createImplementation(). Register as <Api.Extension>.
YOU MUST include the full file path with the .ts extension in every src prop. For example, use src={"/extensions/MySchema.ts"}, NOT src={"/extensions/MySchema"}. 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.
The execute method receives a schema builder and returns it after adding type defs and resolvers.
// extensions/mySchema/MyGraphQLSchema.ts
import { GraphQLSchemaFactory } from "webiny/api/graphql";
class MySchema implements GraphQLSchemaFactory.Interface {
async execute(
builder: GraphQLSchemaFactory.SchemaBuilder
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
builder.addTypeDefs(/* GraphQL */ `
extend type Query {
hello: String!
}
`);
builder.addResolver({
path: "Query.hello",
resolver: () => {
return () => "Hello, World!";
}
});
return builder;
}
}
export default GraphQLSchemaFactory.createImplementation({
implementation: MySchema,
dependencies: []
});
Register as an extension:
// extensions/mySchema/Extension.tsx
import React from "react";
import { Api } from "webiny/extensions";
export const MySchema = () => {
return <Api.Extension src={"@/extensions/mySchema/MyGraphQLSchema.ts"} />;
};
| Method | Description |
|---|---|
builder.addTypeDefs(typeDefs: string) |
Add GraphQL type definitions (use extend type Query/Mutation to add to existing root types) |
builder.addResolver<TArgs>(config) |
Add a resolver with optional per-resolver DI dependencies |
addResolver Configbuilder.addResolver<TArgs>({
path: "TypeName.fieldName", // dot-separated path
dependencies: [SomeAbstraction], // optional: DI tokens resolved at request time
resolver: (dep1, dep2, ...) => { // factory: receives resolved deps
return ({ parent, args, context, info }) => {
// actual resolver logic
return result;
};
}
});
Key points:
path: Dot-separated GraphQL type path, e.g. "Query.hello", "Mutation.createOrder", "OrderMutation.create"
dependencies: Array of DI abstraction tokens. Resolved per-request from context.container, not at schema build timeresolver: A factory function that receives resolved dependencies and returns the actual resolver function{ parent, args, context, info } (named object, not positional)Dependencies in addResolver are resolved at request time from the request-scoped container. This is different from class-level constructor DI — it gives each resolver access to request-scoped services like identity and tenant context.
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.
Full pattern using Response / ErrorResponse wrappers and UseCase injection:
import { Response } from "@webiny/handler-graphql";
import { ErrorResponse } from "@webiny/handler-graphql";
import { GraphQLSchemaFactory } from "@webiny/handler-graphql/graphql/abstractions.js";
import { GetCurrentEntityUseCase } from "../features/getCurrentEntity/abstractions.js";
class GetCurrentEntitySchema implements GraphQLSchemaFactory.Interface {
async execute(
builder: GraphQLSchemaFactory.SchemaBuilder
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
builder.addTypeDefs(/* GraphQL */ `
type EntityResponse {
data: Entity
error: Error
}
type Entity {
id: ID!
values: JSON!
}
type MyPackageQuery {
getCurrentEntity: EntityResponse
}
extend type Query {
myPackage: MyPackageQuery
}
`);
// Pass-through resolver for the namespace
builder.addResolver({
path: "Query.myPackage",
resolver: () => {
return () => ({});
}
});
builder.addResolver({
path: "MyPackageQuery.getCurrentEntity",
dependencies: [GetCurrentEntityUseCase],
resolver: (getEntity: GetCurrentEntityUseCase.Interface) => {
return async () => {
const result = await getEntity.execute();
if (result.isFail()) {
return new ErrorResponse(result.error);
}
return new Response(result.value);
};
}
});
return builder;
}
}
export default GraphQLSchemaFactory.createImplementation({
implementation: GetCurrentEntitySchema,
dependencies: []
});
For namespaced mutations (e.g. mutation { myPackage { createEntity } }):
Mutation
// Schema 1: defines the namespace
builder.addTypeDefs(/* GraphQL */ `
type MyPackageMutation {
_empty: String
}
extend type Mutation {
myPackage: MyPackageMutation
}
`);
builder.addResolver({
path: "Mutation.myPackage",
resolver: () => {
return () => ({});
}
});
// Schema 2: extends the namespace
builder.addTypeDefs(/* GraphQL */ `
extend type MyPackageMutation {
disableEntity(entityId: ID!): BooleanResponse
}
`);
builder.addResolver<{ entityId: string }>({
path: "MyPackageMutation.disableEntity",
dependencies: [DisableEntityUseCase],
resolver: (disableEntity: DisableEntityUseCase.Interface) => {
return async ({ args }) => {
const result = await disableEntity.execute(args.entityId);
if (result.isFail()) {
return new ErrorResponse(result.error);
}
return new Response(true);
};
}
});
When GraphQL inputs must reflect CMS model fields (e.g., an extensible "extensions" object):
import { GraphQLSchemaFactory } from "@webiny/handler-graphql/graphql/abstractions.js";
import { Response, ErrorResponse } from "@webiny/handler-graphql";
import { PluginsContainer } from "@webiny/api-headless-cms/legacy/abstractions.js";
import { renderInputFields } from "@webiny/api-headless-cms/utils/renderInputFields.js";
import { createFieldTypePluginRecords } from "@webiny/api-headless-cms/graphql/schema/createFieldTypePluginRecords.js";
import { ListModelsUseCase } from "@webiny/api-headless-cms/exports/api/cms/model.js";
import { CreateEntityUseCase } from "../features/createEntity/abstractions.js";
import { ENTITY_MODEL_ID } from "~/shared/constants.js";
class CreateEntitySchema implements GraphQLSchemaFactory.Interface {
constructor(
private pluginsContainer: PluginsContainer.Interface,
private listModelsUseCase: ListModelsUseCase.Interface
) {}
async execute(
builder: GraphQLSchemaFactory.SchemaBuilder
): Promise<GraphQLSchemaFactory.SchemaBuilder> {
const inputCreateFields = await this.getExtensionsInput();
builder.addTypeDefs(/* GraphQL */ `
${inputCreateFields.map(f => f.typeDefs).join("\n")}
input CreateEntityInput {
id: ID
name: String!
description: String
${inputCreateFields.map(f => f.fields).join("\n")}
}
extend type MyPackageMutation {
createEntity(input: CreateEntityInput!): BooleanResponse
}
`);
builder.addResolver<{ input: CreateEntityUseCase.Input }>({
path: "MyPackageMutation.createEntity",
dependencies: [CreateEntityUseCase],
resolver: (createEntity: CreateEntityUseCase.Interface) => {
return async ({ args }) => {
const result = await createEntity.execute(args.input);
if (result.isFail()) {
return new ErrorResponse(result.error);
}
return new Response(true);
};
}
});
return builder;
}
private async getExtensionsInput() {
const fieldTypePlugins = createFieldTypePluginRecords(this.pluginsContainer);
const modelsResult = await this.listModelsUseCase.execute({
includePlugins: true,
includePrivate: false
});
if (modelsResult.isFail()) {
return [{ typeDefs: "", fields: "extensions: JSON" }];
}
const models = modelsResult.value;
const model = models.find(m => m.modelId === ENTITY_MODEL_ID)!;
return renderInputFields({
models,
model,
fields: model.fields.filter(f => f.fieldId === "extensions"),
fieldTypePlugins
});
}
}
// Note: constructor DI needed here because of PluginsContainer + ListModelsUseCase
export default GraphQLSchemaFactory.createImplementation({
implementation: CreateEntitySchema,
dependencies: [PluginsContainer, ListModelsUseCase]
});
When your package needs CMS access, implement a PermissionTransformer to expand your custom permission into the required CMS permissions:
// features/addCmsPermissions/AddCmsPermissions.ts
import { PermissionTransformer } from "@webiny/api-core/features/security/authorization/AuthorizationContext/abstractions.js";
class AddCmsPermissions implements PermissionTransformer.Interface {
execute(permission: PermissionTransformer.Permission) {
if (permission.name !== "mypackage.*") {
return permission;
}
return [
permission,
{ name: "cms.endpoint.manage" },
{ name: "cms.contentModel", own: false, rwd: "r", pw: "", models: ["myEntityModelId"] },
{ name: "cms.contentModelGroup", own: false, rwd: "r", pw: "", groups: ["hidden"] },
{ name: "cms.contentEntry", own: false, rwd: "rwd", pw: "" }
];
}
}
export default PermissionTransformer.createImplementation({
implementation: AddCmsPermissions,
dependencies: []
});
GraphQLSchemaFactory.Interface
builder.addTypeDefs() for schema definitions and builder.addResolver() for resolversdependencies array lists DI abstractions; resolver function receives resolved instances in same orderbuilder.addResolver<{ input: UseCaseAbstraction.Input }>
MyPackageQuery, MyPackageMutation) extended by individual schemasResponse for success, ErrorResponse for failure (from @webiny/handler-graphql)default
Import: import { GraphQLSchemaFactory } from "webiny/api/graphql";
Interface: GraphQLSchemaFactory.Interface
Builder: GraphQLSchemaFactory.SchemaBuilder (param type for execute)
Return: Promise<GraphQLSchemaFactory.SchemaBuilder>
Export: GraphQLSchemaFactory.createImplementation({ implementation, dependencies })
Register: <Api.Extension src={"@/extensions/mySchema/MyGraphQLSchema.ts"} />
Deploy: yarn webiny deploy api --env=dev
Response: import { Response, ErrorResponse } from "@webiny/handler-graphql"
webiny.config.tsx