Sitecore GraphQL best practices

Abstract

Recommended best practices when using Sitecore GraphQL, including how to improve performance.

When working with the Sitecore GraphQL API we recommend you adhere to the following best practices:

Use a context-aware endpoint

For public sites, use a database context-aware endpoint to respect the correct content database (master/web/etc). This makes your API return appropriate item data in authenticated modes (preview, Experience Editor).

To make an endpoint context-aware:

  • Ensure that  the endpoint is defined as type Sitecore.Services.GraphQL.Hosting.DatabaseAwareGraphQLEndpoint, Sitecore.Services.GraphQL.NetFxHost

  • When configuring the ContentSchemaProvider’s database setting, set it to context instead of an actual database name. This makes it follow Sitecore.Context.Database.

  • Note that a context-aware endpoint serves a different schema per database, so queries you validate against master might not validate against web if the templates behind them are not published. 

  • If an endpoint is not serving any database-specific content,  do not make it a context-aware endpoint. This can be, for example,  an endpoint you specify only to connect to a CRM or to push analytics data,

Do not use subscriptions on public websites

Do not use GraphQL subscriptions on public websites.

Subscriptions are intended to be used for Content Management server customizations used by authors. Sitecore does not support using subscriptions in scaled public environments.

There are many options for how you define fields in your own IGraphType implementations.

In general, the best-performing and most flexible option is to use the Field method, specify a graph type explicitly, and use a named resolver function. This way, you get good stack traces when errors occur. The following is an example:

// the <ItemState> determines the data object type this graph type maps to
// (in a resolver, context.Source is of this type)
public class ItemWorkflowGraphType : ObjectGraphType<ItemState>
{
    public ItemWorkflowGraphType()
    {
        // ALWAYS set the name. Note that the name must be unique within a schema so be descriptive.
        Name = "ItemWorkflow"; 

        // define your field (note: wrap type in NonNullGraphType<T> if it should never be null)
        Field<ItemWorkflowStateGraphType>("workflowState", resolve: ResolveWorkflowState);

        // this would automatically map to a property on the ItemState called WorkflowState
        // DO NOT use this format, because it causes reflection during queries = slow
        Field<ItemWorkflowStateGraphType>("workflowState");

        // Expression-based resolver. Reasonable performance, non-verbose, but cannot specify
        // the graph type (or its nullablilty), and you do not get nice stack traces on error
        // useful for very simple scalar type resolution (e.g. strings, ints)
        Field("workflowState", state => GetWorkflowState());

        // Sometimes you might not have access to the Field() method, in which case 
        // you can use manual field-adding syntax. This example is equivalent to the first recommended one.
        // Note: DO NOT set the ResolvedType property by accident. This will mess things up.
        AddField(new FieldType
        {
            Name = "workflowState",
            Type = typeof(ItemWorkflowStateGraphType),
            Resolver = new FuncFieldResolver<ItemState, WorkflowState>(ResolveWorkflowState)
        });
    }

    // explicit named resolver function means that you will see a reasonable stack trace if a resolve error occurs
    // (as oppposed to an anonymous function in the constructor)
    private WorkflowState ResolveWorkflowState(ResolveFieldContext<ItemState> context)
    {
        return context.Source.GetWorkflowState();
    }
}

Dealing with arbitrary hierarchies

Some kinds of data are not easy to use with GraphQL, especially arbitrarily nested hierarchies.

An example is if you want to get all the descendants of an item, and the number of levels of children is unknown: GraphQL does not support this because you must specify the part of the graph you need.

Another case is where you always want a set of fields together, or not at all, (for example, a block of arbitrary JSON used by a rendering toolkit, and you do not want to make your users memorize GraphQL fragments). In this case, you can use the JsonGraphType. This type allows you to return arbitrary JSON as a GraphQL field. This is the last resort option when a graph does not make a good representation of your data. A resolver for JsonGraphType can return any object (serialized) or a JToken (used as is).

The JsonGraphType can also be used as an input graph type, in which case you must pass the value as an escaped string. This differs from an output type, where actual JSON is returned without escaping.

When you consume the GraphQL API from the front end, we recommend the following:

  • When using the GraphQL API for a frontend site or application, always define your own endpoint for that site. This gives you fine-grained control over the attack surface, authentication, and URLs of your API. Expose as small of an API as possible.

  • Reuse schema providers, such as ContentSchemaProvider if they already exist.

  • Separate your queries into .graphql files. Do not mix them with your code.

    • This gives a good separation of concerns, and with a tool like graphql-tag/loader, you can import the file in the same way as a JavaScript file.

    • Statically analyzable queries make things easier to validate all your queries at build time, run a security whitelist, and do other common operations.

    • When this is not possible (for example, currently with Angular, you cannot customize the build to add the GraphQL loader), separate your queries into .js or .ts files that contain only queries, for example, mycomponent.graphql.ts for Angular components.

  • Never use dynamic string-concatenated queries (with non-GraphQL variables in the query text). Query variables must always be GraphQL query variables.

    • This defeats whitelisting, static analysis, performance analysis, and is generally just a bad idea.

  • Take advantage of query batching:

    • One of the major advantages of GraphQL over REST is that because GraphQL is a protocol, it makes it possible to do some things REST cannot do.

    • Query batching makes it possible to automatically combine multiple queries (made within a short time span) into a single HTTP request.

  • Use tools to ensure your GraphQL queries are valid against the GraphQL schema, such as eslint-plugin-graphql.

    • This provides build-time safety that your query expressions are valid and will not break when run.

Many kinds of GraphQL tools (such as eslint-plugin-graphql to validate queries at build time, graphql-tools to create a disconnected mock GraphQL API, or ts-graphql-plugin to provide code completion of GraphQL in TypeScript) require a copy of your GraphQL Schema to execute correctly. In some cases, you can download this directly from a Sitecore instance to have a live schema. However, there is no live Sitecore instance in other cases, so you must have a static copy of the schema.

There are two main types of schema input:

  • A JSON-formatted schema. This is the result of an introspection query against the GraphQL API. This is what GraphiQL uses to give documentation in the browser, for example.

  • A schema-definition language schema. This is a text format that defines the schema in a readable format. You can download this by visiting $endpointUrl/schema to get the content, then save it as a .graphql file.

Changes in the Sitecore setup (such as changing or adding templates) result in the GraphQL schema changing. When you use a static schema file, you must ensure it stays updated to avoid false validations.