WHAT YOU'LL LEARN
  • how to add custom GraphQL types, queries, and mutations
  • how to register resolvers with dependency injection
  • how to handle errors in GraphQL responses using the Result pattern

Overview
anchor

Webiny exposes a GraphQL API for all its core applications. You can extend it with your own types, queries, and mutations by implementing the GraphQLSchemaFactory interface. The factory receives a schema builder, you call addTypeDefs() and addResolver() on it, and Webiny merges your additions into the running schema.

The GraphQL layer stays thin: resolvers delegate to use cases and services that carry the business logic.

Adding a Custom Query
anchor

Create an extension file and implement GraphQLSchemaFactory.Interface:

extensions/listCmsEntriesGraphQL.ts
import { GraphQLSchemaFactory } from "webiny/api/graphql";
import { GetModelUseCase } from "webiny/api/cms/model";
import { ListPublishedEntriesUseCase } from "webiny/api/cms/entry";

interface IListCmsEntriesArgs {
  modelId: string;
  limit?: number;
  after?: string;
}

class ListCmsEntriesGraphQL implements GraphQLSchemaFactory.Interface {
  public async execute(builder: GraphQLSchemaFactory.SchemaBuilder): GraphQLSchemaFactory.Return {
    builder.addTypeDefs(/* GraphQL */ `
      type CustomListCmsEntriesResponseItem {
        id: ID!
        title: String!
      }
      type CustomListCmsEntriesResponse {
        data: [CustomListCmsEntriesResponseItem!]
        meta: CmsListMeta
        error: CmsError
      }

      extend type Query {
        listCmsEntries(modelId: ID!, limit: Int, after: String): CustomListCmsEntriesResponse!
      }
    `);

    builder.addResolver<IListCmsEntriesArgs>({
      path: "Query.listCmsEntries",
      dependencies: [GetModelUseCase, ListPublishedEntriesUseCase],
      resolver(
        getModel: GetModelUseCase.Interface,
        listEntries: ListPublishedEntriesUseCase.Interface
      ) {
        return async ({ args }) => {
          const { modelId, limit, after } = args;

          const modelResult = await getModel.execute(modelId);
          if (modelResult.isFail()) {
            return { error: modelResult.error, data: null, meta: null };
          }
          const model = modelResult.value;

          const entriesResult = await listEntries.execute(model, {
            limit: limit ?? 10,
            after: after ?? null
          });
          if (entriesResult.isFail()) {
            return { error: entriesResult.error, data: null, meta: null };
          }

          const { entries, meta } = entriesResult.value;
          return {
            data: entries.map(item => ({
              id: item.id,
              title: item.values[model.titleFieldId] || "No title"
            })),
            meta,
            error: null
          };
        };
      }
    });

    return builder;
  }
}

export default GraphQLSchemaFactory.createImplementation({
  implementation: ListCmsEntriesGraphQL,
  dependencies: []
});

Then register it in webiny.config.tsx:

webiny.config.tsx
import React from "react";
import { Api } from "webiny/extensions";

export const Extensions = () => {
  return (
    <>
      {/* ... other extensions */}
      <Api.Extension src={"/extensions/listCmsEntriesGraphQL.ts"} />
    </>
  );
};

How It Works
anchor

  • addTypeDefs() — accepts a GraphQL SDL string (use the /* GraphQL */ tag for editor syntax highlighting). Extend built-in types with extend type Query or extend type Mutation.
  • addResolver() — registers a resolver for a path in the schema (e.g. "Query.listCmsEntries"). The dependencies array lists the DI tokens that Webiny injects into the resolver factory in the same order.
  • Result pattern — all use cases return a Result object. Check result.isFail() before accessing result.value, and return the error from the GraphQL response so clients receive structured error information.

Adding a Custom Mutation
anchor

The same pattern applies for mutations — extend Mutation instead of Query:

extensions/logMyClickGraphQL.ts
import { GraphQLSchemaFactory } from "webiny/api/graphql";
import { GetModelUseCase } from "webiny/api/cms/model";
import { CreateEntryUseCase } from "webiny/api/cms/entry";

interface ILogMyClickArgs {
  id: string;
  ip: string;
}

class LogMyClickGraphQL implements GraphQLSchemaFactory.Interface {
  public async execute(builder: GraphQLSchemaFactory.SchemaBuilder): GraphQLSchemaFactory.Return {
    builder.addTypeDefs(/* GraphQL */ `
      type LogMyClickResponseItem {
        id: ID!
        ip: String!
        createdOn: String!
      }
      type LogMyClickResponse {
        data: LogMyClickResponseItem
        error: CmsError
      }

      extend type Mutation {
        logMyClick(id: ID!, ip: String!): LogMyClickResponse!
      }
    `);

    builder.addResolver<ILogMyClickArgs>({
      path: "Mutation.logMyClick",
      dependencies: [GetModelUseCase, CreateEntryUseCase],
      resolver(getModel: GetModelUseCase.Interface, createEntry: CreateEntryUseCase.Interface) {
        return async ({ args }) => {
          const modelResult = await getModel.execute("logMyClickModel");
          if (modelResult.isFail()) {
            return { error: modelResult.error, data: null };
          }
          const model = modelResult.value;

          const result = await createEntry.execute<ILogMyClickArgs>(model, {
            values: { id: args.id, ip: args.ip }
          });
          if (result.isFail()) {
            return { error: result.error, data: null };
          }
          return { data: result.value, error: null };
        };
      }
    });

    return builder;
  }
}

export default GraphQLSchemaFactory.createImplementation({
  implementation: LogMyClickGraphQL,
  dependencies: []
});

Deploying Changes
anchor

After creating or modifying a GraphQL extension, deploy the API:

yarn webiny deploy api

During development, use watch mode for automatic redeployment:

yarn webiny watch api