Trendy Coder Logo
  • Home
  • About
  • Blog
  • Newsletter

GraphQL Optimization in Payload CMS

Posted on: April 04 2025
By Dave Becker
Hero image for GraphQL Optimization in Payload CMS

In today's modern web development landscape, GraphQL has become a powerful tool for fetching and manipulating data efficiently. When combined with Payload CMS, a headless content management system, it offers developers a flexible and scalable approach to query content.

Payload CMS supports seamless integration with GraphQL. In this tutorial, I'll cover some of the techniques that I've used to leverage GraphQL more effectively.

In this article I'll be covering the following:

  • Why use GraphQL with Payload?
  • Visual Studio Code and GraphQL
  • How to setup Next.js to support GraphQL
  • Generate GraphQL schema in Payload CMS.
  • Write GraphQL queries for nested relationships.
  • How to test out your GraphQL queries.
  • When to use GraphQL, REST or Local?

Why use Payload and GraphQL

When I first started using Payload, I felt that REST API's were all I was going to need and as I began to write more complex content, GraphQL seemed more and more like the right tool for the job.

Payload has great support for GraphQL and it provides some benefits:

  • More flexibility over what data is returned.
  • Payload generates GraphQL schema and types based on Collection configurations.
  • Reduces response sizes when multiple relationships and joins are used.
  • Supports Mutations which are similar to REST Post and Update.

Optimizing GraphQL on Payload

GraphQL has great benefits but writing queries using strings can become hard to manage and make the experience less enjoyable.

I also felt that writing queries using strings didn't take advantage of the strongly typed GraphQL schema that Payload was generating.

I felt a more optimized strategy should include the following:

  • Utilize Payload's generated GraphQL schema.
  • Use a Visual Studio plugin for full intellisense.
  • Avoid writing string based queries since they can be error prone and harder to maintain and write.
  • Import GraphQL queries directly into pages.
  • Centralize (or co-locate) queries for better organization rather than completely inline queries.
  • Lastly, generate all the needed queries and mutations based on the schema without having to write anything unless needed.

So based on these requirements, I'll walk through some of these strategies to make GraphQL awesome on Payload.

Setting up GraphQL schema

In order to import and generate the Payload GraphQL schema, add the following modules to the package.json.

pnpm add @payloadcms/graphql graphql-tag -D

Also, add the graphql module as a dependency.

pnpm add graphql

Once installed, the generate command can be called using:

pnpm payload-graphql generate:schema

Let's also add a script in the package.json.

{
  "scripts": {
      "generate:graphql-schema": "payload-graphql generate:schema"
  }
}

After running the GraphQL schema, it will output the schema file in the root directory of the project.

Importing GraphQL extensions

In order to actually import a file that contains GraphQL query text we need to configure Next.js to recognize and load graphql files.

Adding the following webpack loader to the next.config.mjs file will instruct the compiler on how to handle these import types.

  • *.graphql
  • *.gql
next config
import { withPayload } from '@payloadcms/next/withPayload'

/** @type {import('next').NextConfig} */
const nextConfig = {
  // ... 
  webpack: (config) => {
    config.module.rules.push({
      test: /\.(graphql|gql)$/,
      exclude: /node_modules/,
      loader: 'graphql-tag/loader',
    });

    return config;
  },
  // ...
}

export default withPayload(nextConfig)

TypeScript and GraphQL modules

Since we're adding a new type of module import, TypeScript is going to complain and show some red squiggly lines in the IDE.

To satsify TypeScript we'll need to add the following type declarations to a *.d.ts file.

In this case I'm just using globals.d.ts but you can name it whatever makes sense or add it to an existing delcaration file.


declare module '*.graphql' {
  import { DocumentNode } from 'graphql'
  const Schema: DocumentNode

  export = Schema
}

declare module '*.gql' {
  import { DocumentNode } from 'graphql'
  const Schema: DocumentNode

  export = Schema
}

Now we need to let TypeScript know where it can find the GraphQL declarations, by adding the path to the include array section of the tsconfig.json configuration.

tsconfig
{
  "include": ["global.d.ts"],
}

Visual Studio Code GraphQL support

In order for the VS Code IDE to know how to diplay and handle GraphQL files types, the GraphQL Foundation provides the following extensions.

  • GraphQL: Syntax Highlighting
  • GraphQL: Language Feature Support

These extensions really make writing GraphQL queries and mutations awesome and much less error prone than using strings.

Point to the Payload GraphQL schema

Now we can finally tie everything together and use the actual Payload generated schema.graphql file.

Add the following configuration snippet to your package.json file.

This is where the GraphQL extensions will look for schema definitions and file extensions that need GraphQL support.

package.json
{
   "graphql": {
    "schema": "schema.graphql",
    "documents": "**/*.{graphql,gql,js,ts,jsx,tsx}"
  }
}

Now all the GraphQL pages will have the following enhancements:

  • File icon
  • Intellisense and code completion
  • Syntax highlighting

Create a Payload Collection

In order to fully demonstrate why GraphQL is an important part of Payload, I'll write a query that accomplishes the following:

  • Queries a pages Collection
  • Queries any content blocks defined in the layout builder.
  • Query all block relationships and media content.

I recently posted an article on how to use Payload's content blocks Maximizing Efficiency: Unlocking the Power of Payload Content Blocks. It's a good starting point since I will be using GraphQL to query Payload blocks in this article rather than REST.

Define a Payload Collection page with blocks

Here's a basic version of the pages Collection which defines two blocks, Banner and Content.

Page Collection with blocks
import type { CollectionConfig } from 'payload'
import Banner from '../../blocks/Banner/config'
import Content from '../../blocks/Content/config'

export const Pages: CollectionConfig = {
  slug: 'pages',
  fields: [
    {
      name: 'slug',
      type: 'text',
    },
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'description',
      type: 'text',
    },
    {
      name: 'layouts',
      type: 'blocks',
      required: false,
      blocks: [Banner, Content],
    },
  ],
}

Banner blocks definition

The Banner block contains a few relationships and nested links to other pages so it will be a good example of a GraphQL query.

Banner block content example
import type { Block } from 'payload'

import { lexicalEditor } from '@payloadcms/richtext-lexical'

const Banner: Block = {
  slug: 'banner',
  interfaceName: 'BannerBlock',
  fields: [
    {
      name: 'media',
      label: 'Background Image',
      type: 'upload',
      relationTo: 'media',
      required: true,
    },
    {
      name: 'richText',
      type: 'richText',
      editor: lexicalEditor(),
      label: 'Content',
    },
    {
      name: 'links',
      interfaceName: 'LinkGroupType',
      label: 'Links',
      type: 'array',
      fields: [
        {
          name: 'link',
          interfaceName: 'LinkType',
          type: 'group',
          admin: {
            hideGutter: true,
          },
          fields: [
            {
              name: 'displayText',
              label: 'Link Text',
              type: 'text',
              required: true
            },
            {
              type: 'row',
              fields: [
                {
                  name: 'type',
                  type: 'radio',
                  defaultValue: 'reference',
                  options: [
                    {
                      label: 'Internal link',
                      value: 'reference',
                    },
                    {
                      label: 'Custom URL',
                      value: 'custom',
                    },
                  ],
                },
                {
                  name: 'reference',
                  type: 'relationship',
                  label: 'Document to link to',
                  relationTo: ['pages'],
                  required: true,
                },
                {
                  name: 'url',
                  type: 'text',
                  label: 'Custom URL',
                  required: true,
                },
                
                {
                  name: 'newTab',
                  type: 'checkbox',
                  label: 'Open in new tab',
                },
              ],
            },
          ],
        },
      ],
    },
  ],
}
export default Banner

Content blocks definition

This Content block configuration is quite simple but it will show how to query multiple blocks.

Content block example
import type { Block } from 'payload'
import {
  lexicalEditor,
} from '@payloadcms/richtext-lexical'

const Content: Block = {
  slug: 'content',
  interfaceName: 'ContentBlock',
  fields: [
    {
      name: 'richText',
      type: 'richText',
      editor: lexicalEditor(),
      label: false,
    },
  ],
}
export default Content;

Writing GraphQL queries

Now that the Collection and blocks are available to query, I'll create the following GraphQL queries:

  • Page.gql: Queries for a page by ID.
  • PageBySlug.gql: Query for page by slug name, which is based on a where clause.

By holding down the Cmd key, and clicking on any type in VS Code IDE, it will instantly open up the schema.graphql and point to the definition.

As you build your queries now, the VS Code plugin will show intellisense while typing.

Show graphql intellisense when building queries with VS Code plugin.

Query Page by ID

// outer is the query name and parameters
query Page($id: Int!, $draft: Boolean) {
  // inner indicates which Payload schema.graphql type to use.
  Page(id: $id, draft: $draft) { 
     // fields here
  }
}

Here's the full query for the Page.gql.

Page.gql
query Page($id: Int!, $draft: Boolean) {
  Page(id: $id, draft: $draft) {
    id
    slug
    title
    description
    layouts {
      ... on BannerBlock {
        id
        media {
          id
          width
          height
          alt
          url
          filename
          mimeType
          thumbnailURL
        }
        links {
          link {
            displayText
            type
            reference {
              relationTo
              value {
                ... on Page {
                  title
                  description
                  slug
                  id
                }
              }
            }
            url
            newTab
          }
        }
        blockName
        blockType
      }
      ... on ContentBlock {
        id
        richText
        blockName
        blockType
      }
    }
  }
}

A couple things to note here:

  • layouts uses the ... on BannerBlock and ... on ContentBlock notation, which is available from the generated schema because each block defines an interfaceName in it's configuration.
  • Any relationship fields will have a reference key and can use the ... on <type> notation.

Here's an example of how Payload structures relationship fields.

Example using ... on
 {
  reference {
    relationTo
    value {
      ... on <type> {
       // fields here
      }
    }
  }

Using the ... on <type> is very useful when building blocks and relationship queries.

Query Page by slug

Here's the query by slug example. This is useful when you want to find a document page by a more SEO optimized name.

// outer is the query name and parameters
query Page($slug: String, $draft: Boolean) {
  
  // inner indicates which Payload schema.graphql type to use which is "Pages", not to be confused with "Page" singular.
  Pages(where: { AND: [{ slug: { equals: $slug } }] }, limit: 1, draft: $draft) {
     // pagination "docs" array which is required for "where" conditions. 
     docs { 
       // fields here
     }
  }
}

This example actually uses a where condition.

where: { AND: [{ slug: { equals: $slug } }] }

Anytime, a GraphQL or REST query uses a where condition, its response will return a PaginatedDocs type which contains a docs array and other pagination details.

So we'll also need to add the docs notation in the actual query.

Here's the full PageBySlug.gql query example.

PageBySlug.gql
query Page($slug: String, $draft: Boolean) {
  Pages(where: { AND: [{ slug: { equals: $slug } }] }, limit: 1, draft: $draft) {
    docs {
      id
      slug
      title
      description
      layouts {
        ... on BannerBlock {
          id
          media {
            id
            width
            height
            alt
            url
            filename
            mimeType
            thumbnailURL
          }
          links {
            link {
              displayText
              type
              reference {
                relationTo
                value {
                  ... on Page {
                    title
                    description
                    slug
                    id
                  }
                }
              }
              url
              newTab
            }
          }
          blockName
          blockType
        }
        ... on ContentBlock {
          id
          richText
          blockName
          blockType
        }
      }
    }
  }
}

The query by slug is very similar to the query by id with the only difference being the where and docs syntax changes.

Testing GraphQL queries

When running Payload in development mode, it provides the graphql-playground endpoint.

http://localhost:3000/api/graphql-playground

This is an excellent tool to try out your queries to see if they are working and returning the data as you expect.

Here's the GraphQL playground UI for testing.

Shows Payload's graphql playground UI for testing queries

This tools is already wired up to use the schema.graphql generated by Payload so you can build queries directly inside of this UI interface.

I personally prefer to build my queries in VS Code and then copy/paste them into the playground interface.

Accessing GraphQL from Next.js pages

Here's a couple of Next.js pages that will demonstrate how to use and access Payload's graphql endpoint.

Every GraphQL query is based on a POST request, where the body of the request is the actual query along with variables.

A couple things to note are:

  • The request header Content-Type needs to be set to application/json.
  • Make sure any variables are the proper primitive JavaScript type that the GraphQL query expects.
  • The import statement for each query, will return an AST tree representation of the GraphQL query and needs to be deserialized to a string using the graphql print helper function.

The response objects for each query are a bit different.

Query by ID response format
{
    "data": {
        "Page": {
        }
    }
}

The reponse for any where conditions will also send the docs array.

Query by slug response format
{
    "data": {
        "Pages": {
           "docs": []
        }
    }
}

Next.js page to access by ID

This page will look for the [id] path param in the url.

import React from 'react'
import Page from '@/payload/graphql/Page.gql'
import { print } from 'graphql'
import { Page as PageType } from '@/payload-types'

type Props = {
  params: Promise<{
    id?: string
  }>
}

export default async function DemoPage({ params: paramsPromise }: Props) {
  const { id } = await paramsPromise

  let page: PageType = await fetch('http://localhost:3000/api/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: print(Page),
      variables: {
        id: Number(id),
      },
    }),
  })
    .then((res) => res.json())
    .then((res) => {
      if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')

      return res?.data?.['Page']
    })
  return <pre>{JSON.stringify(page, null, 2)}</pre>
}

Next.js page to access by slug

This page will look for the [slug] path param in the url.

import React from 'react'
import PageBySlug from '@/payload/graphql/PageBySlug.gql'
import { print } from 'graphql'
import { Page as PageType } from '@/payload-types'

type Props = {
  params: Promise<{
    slug?: string
  }>
}

export default async function DemoPage({ params: paramsPromise }: Props) {
  const { slug } = await paramsPromise

  let page: PageType = await fetch('http://localhost:3000/api/graphql', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      query: print(PageBySlug),
      variables: {
        slug,
      },
    }),
  })
    .then((res) => res.json())
    .then((res) => {
      if (res.errors) throw new Error(res?.errors?.[0]?.message ?? 'Error fetching doc')

      return res?.data?.['Pages']?.docs?.[0]
    })
  return <pre>{JSON.stringify(page, null, 2)}</pre>
}

Automatically Generating GraphQL queries

The last topic worth mentioning is how to generate all the queries and mutations without having to write them by hand.

This is can be useful if you don't need any customizations in your queries. Generated queries will pull all data that is mapped by the schema.

In order to generate the graphql.schema into queries and mutations, we'll need to use a generator.

pnpm add gql-generator -D

Let's also add some package.json scripts to orchestrate the process and do the following:

  • generate the Payload schema
  • cleanup the generated output folder before re-generating.
  • generate the schema to the output folder.

All of the generated code will go into the /src/payload/graphql/gen directory, so it does not interfere with any custom queries in the parent directory /src/payload/graphql.

{
  "generate:schema": "payload-graphql generate:schema",
  "generate:graphql-clean": "rm -rf ./src/payload/graphql/gen/**",
  "generate:graphql-queries": "pnpm generate:graphql-schema && pnpm generate:graphql-clean && gqlg --schemaFilePath ./schema.graphql --destDirPath ./src/payload/graphql/gen --depthLimit 10",
}

Next, just run the following.

pnpm generate:graphql-queries

All of the code will be generated under the output directory in the following subdirectories.

  • queries: All queries for the entire schema.graphql file.
  • mutations: All mutations to modify any schema in the entire schema.graphql file.
Generated GraphQL output files
└── src
    └── payload
        └── graphql
            ├── gen 
            │   ├── queries
            │   ├── mutations
            │   └── index.js
            └── MyCustomQuery.gql 

Simply import the queries into your project from the /gen directory as follows:

// @/payload prefix is compiler paths in tsconfig.ts
import Page from '@/payload/graphql/gen/queries/Page.gql'

In Conclusion

So hopefully this article has offered some improvements to the GraphQL experience when using Payload CMS.

I have found this aproach to using GraphQL very useful for more complex block queries but it can be used for all types of queries.

There is always going to be the case for using an inline string query in GraphQL but having some tools to take the hard part out of writing complex queries is a good thing.

Hope this article has helped to unlock some potential in your projects. Cheers!

Topics

SEOLinuxSecuritySSHEmail MarketingMore posts...

Related Posts

Hero image for Boost Payload CMS with Search: Step-by-Step Tutorial
Posted on: August 11 2025
By Dave Becker
Boost Payload CMS with Search: Step-by-Step Tutorial
Hero image for Create Custom Forms Using Payload CMS Form Builder Plugin
Posted on: August 11 2025
By Dave Becker
Create Custom Forms Using Payload CMS Form Builder Plugin
Hero image for Document Nesting With Payload's Nested Docs Plugin
Posted on: April 04 2025
By Dave Becker
Document Nesting With Payload's Nested Docs Plugin
Hero image for Exploring the Game-Changing Features of Payload CMS 3.0
Posted on: April 04 2025
By Dave Becker
Exploring the Game-Changing Features of Payload CMS 3.0
Hero image for Maximizing Efficiency: The Power of Payload CMS Blocks
Posted on: August 11 2025
By Dave Becker
Maximizing Efficiency: The Power of Payload CMS Blocks
Hero image for Payload CMS Collections: How They Streamline Content Management
Posted on: April 04 2025
By Dave Becker
Payload CMS Collections: How They Streamline Content Management
Hero image for Payload CMS: Getting Started Using the New Join Field
Posted on: August 11 2025
By Dave Becker
Payload CMS: Getting Started Using the New Join Field
Hero image for Payload CMS SEO Plugin: Boosting Your Site's Search Ranking
Posted on: April 04 2025
By Dave Becker
Payload CMS SEO Plugin: Boosting Your Site's Search Ranking
Hero image for Server-Side Pagination Made Easy in Payload CMS
Posted on: August 11 2025
By Dave Becker
Server-Side Pagination Made Easy in Payload CMS
Hero image for Uploading Made Easy: Exploring Media Features in Payload CMS
Posted on: September 23 2025
By Dave Becker
Uploading Made Easy: Exploring Media Features in Payload CMS
Trendy Coder Logo
Resources
  • Blog
Website
  • Home
  • About us
Subscribe

Get the latest news and articles to your inbox periodically.

We respect your email privacy

© TrendyCoder.com. All rights reserved.