Webiny-js webiny-custom-graphql-api
install
source · Clone the upstream repo
git clone https://github.com/webiny/webiny-js
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/webiny/webiny-js "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/user-skills/api/graphql-api" ~/.claude/skills/webiny-webiny-js-webiny-custom-graphql-api && rm -rf "$T"
manifest:
skills/user-skills/api/graphql-api/SKILL.mdsource content
Custom GraphQL API
TL;DR
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
extension in every .ts
prop. For example, use src
src={"/extensions/MySchema.ts"}, NOT src={"/extensions/MySchema"}. Omitting the file extension will cause a build failure.
YOU MUST use
for the export default
call when the file is targeted directly by an Extension createImplementation()
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 GraphQLSchemaFactory Pattern
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"} />; };
Schema Builder API Reference
| Method | Description |
|---|---|
| Add GraphQL type definitions (use to add to existing root types) |
| Add a resolver with optional per-resolver DI dependencies |
addResolver
Config
addResolverbuilder.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:
: Dot-separated GraphQL type path, e.g.path
,"Query.hello"
,"Mutation.createOrder""OrderMutation.create"
: Array of DI abstraction tokens. Resolved per-request fromdependencies
, not at schema build timecontext.container
: A factory function that receives resolved dependencies and returns the actual resolver functionresolver- Resolver params: The inner function receives
(named object, not positional){ parent, args, context, info }
Per-Resolver Dependency Injection
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.
Query Schema with UseCase DI
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: [] });
Namespaced Mutation Pattern
For namespaced mutations (e.g.
mutation { myPackage { createEntity } }):
- One schema defines the base namespace type + extends
Mutation - Other schemas extend the namespace type
// 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); }; } });
Dynamic Input Fields from CMS Model
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] });
Permission Transformer (Adding CMS Permissions)
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: [] });
Key Rules
- Implement
GraphQLSchemaFactory.Interface - Use
for schema definitions andbuilder.addTypeDefs()
for resolversbuilder.addResolver() - Resolver
array lists DI abstractions; resolver function receives resolved instances in same orderdependencies - Type the resolver args generic:
builder.addResolver<{ input: UseCaseAbstraction.Input }> - The root Query/Mutation types define a namespace type (e.g.,
,MyPackageQuery
) extended by individual schemasMyPackageMutation - Use
for success,Response
for failure (fromErrorResponse
)@webiny/handler-graphql - Export as
default
Quick Reference
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"
Related Skills
- webiny-api-architect — Architecture overview, Services vs UseCases, feature naming, anti-patterns
- webiny-use-case-pattern — UseCase implementation consumed by GraphQL resolvers
- webiny-dependency-injection — Full DI reference for all injectable services
- webiny-project-structure — How to register extensions in
webiny.config.tsx