Extend the GraphQL API in Payload Cover

How to extend GraphQL in Payload with custom queries, mutations and re-usable types

PayloadCMS has a great implementation of GraphQL that just works out of the box with enough documentation to get you started and we're going to explain how to extend this API with your own custom queries, mutations and types so that you can avoid some of the confusing moments we had to deal with.

This article is part of a series of working with the Payload APIs:

Directory structure

It's best we start here to establish a strong baseline for best practices as we will do a lot of code splitting.

  • src/

    • graphql/

      • queries/
      • mutations/
      • utilities/
    • payload-config.ts

Now here you can add another directory for resolvers or we can bundle our resolves together with the query or mutation in question, entirely up to you but we will do the latter and remember to check the demo repository for this.

Payload configuration

We will begin with the configuration file:

1// payload-config.ts
2
3import { customGraphQLMutations } from './graphql/mutations'
4import { customGraphQLQueries } from './graphql/queries'
5
6// ...
7
8graphQL: {
9 mutations: customGraphQLMutations,
10 queries: customGraphQLQueries,
11},
12
13// ...
typescript

Operation functions

Each payload operation function will take in the GraphQL object and the payload object and as of writing this the typing for these parameters isn't inferred nor is there a reusable type for the custom functions so we will manually type them in all of our examples.

The return is then expected to be an object consisting of args, type and a resolver which is the function that runs to handle the logic.

1// graphql/queries/index.ts
2
3import { default as ImportedGraphQL } from 'graphql'
4import { Payload } from 'payload'
5import { getMyPosts } from './getMyPosts'
6
7// Custom type inspired by Payload's internal type
8export type customGraphQLQueryType = (GraphQL: typeof ImportedGraphQL, payload: Payload) => Record<string, unknown>
9
10export const customGraphQLQueries: customGraphQLQueryType = (GraphQL, payload) => {
11 return {
12 GetMyPosts: getMyPosts(GraphQL, payload),
13 }
14}
15
typescript

Custom query to get all posts from a user

We're going to build a custom query called GetMyPosts that will return all posts (up to 100) for the logged in user. You could totally write a query with a where argument however there may be situations where you want the developer experience for fetching content like this to be handled server side.

We will expand on some of the types and functions towards the end of the article, but essentially you're responsible for building the types for the args and return of the GraphQL operation and there's a custom utility that we need to use for paginated lists.

1// graphql/queries/getMyPosts/index.ts
2
3import buildPaginatedListType from '../../utilities/buildPaginatedListType'
4import type { default as ImportedGraphQL } from 'graphql'
5import type { Payload } from 'payload'
6import { Resolver } from './resolver'
7
8export const getMyPosts = (GraphQL: typeof ImportedGraphQL, payload: Payload) => {
9 return {
10 args: {},
11 resolve: Resolver,
12 // The name of your new type has to be unique
13 type: buildPaginatedListType('AuthorPosts', payload.collections['posts'].graphQL?.type),
14 }
15}
16
typescript

Resolver

The resolver can take in a few parameters as well including the args which unfortunately cannot currently be inferred easily so we will create a custom type for that.

1// graphql/queries/getMyPosts/resolver.ts
2
3import payload from 'payload'
4
5interface ResolverArgs {}
6
7export const Resolver = async (obj, args: ResolverArgs, { req }, info) => {
8 const author = req.user
9
10 if (!author.id) {
11 throw new Error('Invalid user id')
12 }
13
14 if (author.collection !== 'users') {
15 throw new Error('User could not be verified.')
16 }
17
18 const posts = await payload.find({
19 collection: 'posts',
20 depth: 0, // Necessary for GraphQL to be able to traverse the returned data
21 limit: 100, // Optional, you could allow args to be passed in for this
22 sort: '-createdAt',
23 where: {
24 author: {
25 equals: author.id,
26 },
27 },
28 })
29
30 return posts
31}
32
typescript

And now this query should be available to use in the GraphQL API just like that! Your operation will also show up in the schema with the correct return types automatically and fit right in with the codegens as well.

Screenshot of GetMyPosts query

Now for the mutation

The mutation will be very similar and we're going to create a AddLikeToPost mutation so you can see how a CRUD update would work with GraphQL too.

1// graphql/mutations/index.ts
2
3import { default as ImportedGraphQL } from 'graphql'
4import { Payload } from 'payload'
5import { addLikeToPost } from './addLikeToPost'
6
7// Custom type inspired by Payload's internal type
8export type customGraphQLMutationType = (GraphQL: typeof ImportedGraphQL, payload: Payload) => Record<string, unknown>
9
10export const customGraphQLMutations: customGraphQLMutationType = (GraphQL, payload) => {
11 return {
12 AddLikeToPost: addLikeToPost(GraphQL, payload),
13 }
14}
15
typescript

The difference with our mutation is that we're going to have a non-optional argument for a post ID.

1// graphql/mutations/addLikeToPost/index.ts
2
3import { AddLikeToPostResolver } from './resolver'
4import type { default as ImportedGraphQL } from 'graphql'
5import type { Payload } from 'payload'
6
7export const addLikeToPost = (GraphQL: typeof ImportedGraphQL, payload: Payload) => {
8 return {
9 args: {
10 // List of args for this
11 postId: {
12 type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString), // Important! We construct our graphQL type using the GraphQL passed down via the function
13 },
14 },
15 resolve: AddLikeToPostResolver,
16 type: payload.collections['posts'].graphQL?.type,
17 }
18}
19
typescript

For our resolver we're going to do an additional logic check for the post ID to exist and this is also where you can add more access controls for your custom API endpoints should you need it, eg. limiting actions to specific user roles.

1// graphql/mutations/addLikeToPost/resolver.ts
2
3import payload from 'payload'
4
5interface ResolverArgs {
6 postId: string
7}
8
9export const AddLikeToPostResolver = async (obj, args: ResolverArgs, { req }, info) => {
10 const post = await payload.findByID({
11 collection: 'posts',
12 id: args.postId,
13 })
14
15 if (!post.id) {
16 throw new Error('Invalid post id')
17 }
18
19 const newLikeCount = ++post.likeCount
20
21 const updatedPost = await payload.update({
22 id: post.id,
23 collection: 'posts',
24 data: {
25 likeCount: newLikeCount,
26 },
27 })
28
29 return updatedPost
30}
31
typescript

Returning the updated post here is entirely optional however for the purpose of developer experience it's good practice in this situation, however you could return whatever you want and you just need to make sure it matches with the return type declared in your GraphQL mutation.

Screenshot of AddLikeToPost mutation

Types

All of the collection or global types are exposed for you by default inside the payload parameter, for example for posts we would get the type with payload.collections['post'].graphql.types

Lists

If you want to return a list of documents similar to the default payload result with the totalDocs and other parameters as returned by the local API too, then there is a function for building this type.

In the future this function may be exposed by payload, until then add it to your set of utilities.

1// graphql/utilities/buildPaginatedListType.ts
2
3import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'
4
5const buildPaginatedListType = (name: string, docType: any) =>
6 new GraphQLObjectType({
7 name,
8 fields: {
9 docs: {
10 type: new GraphQLList(docType),
11 },
12 totalDocs: { type: GraphQLInt },
13 offset: { type: GraphQLInt },
14 limit: { type: GraphQLInt },
15 totalPages: { type: GraphQLInt },
16 page: { type: GraphQLInt },
17 pagingCounter: { type: GraphQLInt },
18 hasPrevPage: { type: GraphQLBoolean },
19 hasNextPage: { type: GraphQLBoolean },
20 prevPage: { type: GraphQLInt },
21 nextPage: { type: GraphQLInt },
22 },
23 })
24
25export default buildPaginatedListType
typescript

Input types

Payload also exposes its internally generated input types on the payload.collections['posts'].graphQL if you want to re-use them for your own custom functions and provide the exact same input arguments.

1// Available types on payload.collections['posts'].graphQL
2
3(property) graphQL?: {
4 type: ImportedGraphQL.GraphQLObjectType<any, any>;
5 JWT: ImportedGraphQL.GraphQLObjectType<any, any>;
6 versionType: ImportedGraphQL.GraphQLObjectType<any, any>;
7 whereInputType: ImportedGraphQL.GraphQLInputObjectType;
8 mutationInputType: ImportedGraphQL.GraphQLNonNull<...>;
9 updateMutationInputType: ImportedGraphQL.GraphQLNonNull<...>;
10}
typescript

It's that easy!

And that concludes our mini series on working with and extending Payload's APIs, the full example repository is here.

If you liked this tutorial you can follow us on Twitter for future updates.