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:
- Extending the GraphQL API
- Extending the REST API
- Using codegen with Nextjs and React Query
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.ts23import { customGraphQLMutations } from './graphql/mutations'4import { customGraphQLQueries } from './graphql/queries'56// ...78graphQL: {9 mutations: customGraphQLMutations,10 queries: customGraphQLQueries,11},1213// ...
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.ts23import { default as ImportedGraphQL } from 'graphql'4import { Payload } from 'payload'5import { getMyPosts } from './getMyPosts'67// Custom type inspired by Payload's internal type8export type customGraphQLQueryType = (GraphQL: typeof ImportedGraphQL, payload: Payload) => Record<string, unknown>910export const customGraphQLQueries: customGraphQLQueryType = (GraphQL, payload) => {11 return {12 GetMyPosts: getMyPosts(GraphQL, payload),13 }14}15
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.ts23import buildPaginatedListType from '../../utilities/buildPaginatedListType'4import type { default as ImportedGraphQL } from 'graphql'5import type { Payload } from 'payload'6import { Resolver } from './resolver'78export const getMyPosts = (GraphQL: typeof ImportedGraphQL, payload: Payload) => {9 return {10 args: {},11 resolve: Resolver,12 // The name of your new type has to be unique13 type: buildPaginatedListType('AuthorPosts', payload.collections['posts'].graphQL?.type),14 }15}16
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.ts23import payload from 'payload'45interface ResolverArgs {}67export const Resolver = async (obj, args: ResolverArgs, { req }, info) => {8 const author = req.user910 if (!author.id) {11 throw new Error('Invalid user id')12 }1314 if (author.collection !== 'users') {15 throw new Error('User could not be verified.')16 }1718 const posts = await payload.find({19 collection: 'posts',20 depth: 0, // Necessary for GraphQL to be able to traverse the returned data21 limit: 100, // Optional, you could allow args to be passed in for this22 sort: '-createdAt',23 where: {24 author: {25 equals: author.id,26 },27 },28 })2930 return posts31}32
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.
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.ts23import { default as ImportedGraphQL } from 'graphql'4import { Payload } from 'payload'5import { addLikeToPost } from './addLikeToPost'67// Custom type inspired by Payload's internal type8export type customGraphQLMutationType = (GraphQL: typeof ImportedGraphQL, payload: Payload) => Record<string, unknown>910export const customGraphQLMutations: customGraphQLMutationType = (GraphQL, payload) => {11 return {12 AddLikeToPost: addLikeToPost(GraphQL, payload),13 }14}15
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.ts23import { AddLikeToPostResolver } from './resolver'4import type { default as ImportedGraphQL } from 'graphql'5import type { Payload } from 'payload'67export const addLikeToPost = (GraphQL: typeof ImportedGraphQL, payload: Payload) => {8 return {9 args: {10 // List of args for this11 postId: {12 type: new GraphQL.GraphQLNonNull(GraphQL.GraphQLString), // Important! We construct our graphQL type using the GraphQL passed down via the function13 },14 },15 resolve: AddLikeToPostResolver,16 type: payload.collections['posts'].graphQL?.type,17 }18}19
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.ts23import payload from 'payload'45interface ResolverArgs {6 postId: string7}89export const AddLikeToPostResolver = async (obj, args: ResolverArgs, { req }, info) => {10 const post = await payload.findByID({11 collection: 'posts',12 id: args.postId,13 })1415 if (!post.id) {16 throw new Error('Invalid post id')17 }1819 const newLikeCount = ++post.likeCount2021 const updatedPost = await payload.update({22 id: post.id,23 collection: 'posts',24 data: {25 likeCount: newLikeCount,26 },27 })2829 return updatedPost30}31
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.
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.ts23import { GraphQLBoolean, GraphQLInt, GraphQLList, GraphQLObjectType } from 'graphql'45const 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 })2425export default buildPaginatedListType
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'].graphQL23(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}
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.