This article will teach you how to build a full-stack CRUD App with Next.js, React Query, GraphQL Code Generator, React-Hook-Form, Zod, and graphql-request to perform Create/Update/Get/Delete operations.
Next.js, React Query, and GraphQL Series:
- GraphQL API with Next.js & MongoDB: Access & Refresh Tokens
- GraphQL CRUD API with Next.js, MongoDB, and TypeGraphQL
- Next.js, GraphQL-CodeGen, & React Query: JWT Authentication
- Next.js Full-Stack App with React Query, and GraphQL-CodeGen
More practice:
- React Query, & GraphQL-CodeGen: Access, and Refresh Tokens
- React, Material UI and React Hook Form: Login and Signup Forms
- React, RTK Query, React Hook Form and Material UI – Image Upload
- React + Redux Toolkit: JWT Authentication and Authorization
- React.js + Redux Toolkit: Refresh Tokens Authentication
- Build Vue.js, Vue Query, and Axios CRUD App with RESTful API
Next.js Full-Stack CRUD App Overview
We will build a Next.js, tailwindCss, TypeScript, React-Hook-Form, Zod client with React Query, and graphql-request to make CRUD operations against a GraphQL API.
-On the homepage, a React Query GraphQL request is made to the Next.js GraphQL server to retrieve all the posts.
-To add a new post to the database, click on the “Create Post” link from the navigation menu to display the create post modal.
Next, provide the necessary information and make a React Query GraphQL mutation request to the Next.js GraphQL server to add the post to the database.
-To update a post in the database, click on the three dots on the post to display the update post modal.
Next, edit the fields and make a React Query GraphQL mutation request to the Next.js GraphQL API to update that specific post in the database.
-To remove a post from the database, click on the three dots again and you should be prompted to confirm your action before a React Query GraphQL mutation request is made to the Next.js GraphQL API to remove that particular post from the database.
Benefits of React Query
React Query is a powerful asynchronous server state management library for ReactJs/NextJs. In layman’s terms, it makes fetching, caching, cache synchronizing, and updating server state a breeze in React.js/Next.js applications.
React Query is now an adaptor categorized under TanStackQuery upon the release of version 4.
There are other TanStackQuery adaptors like:
- Vue Query
- Svelte Query
- Solid Query Upcoming
I know there are other server state management libraries like SWR, Apollo Client, and RTK Query but when you carefully analyze the benchmark for the popular server state libraries on the TanStackQuery website, you will notice that React Query outperforms its competitors.
React Query uses a fetching mechanism that is agnostically built on Promises, which makes it compatible with any asynchronous data fetching clients like GraphQL-Request, Axios, FetchAPI, and many more.
Setup GraphQL Code Generator
GraphQL Code Generator is a toolkit tailored to simplify and automate the generation of typed queries, subscriptions, and mutations for React, Next.js, Vue, Angular, Svelte, and other supported frontend frameworks.
To begin, let’s install the GraphQL Code Generator CLI tool with this command:
yarn add -D graphql @graphql-codegen/cli
# or
npm install -D graphql @graphql-codegen/cli
There are two ways to get the GraphQL CodeGen up and running:
- Initialization Wizard – guides you through the whole process of setting up a schema, choosing and installing the required plugins, picking a destination to output the generated files, and many more.
- Manual Setup – gives you the freedom to install plugins and configure them yourself.
However, we will be using the manual process to help you understand what happens under the hood.
Now since we are working with React Query, let’s install the required plugins provided by GraphQL Code Generator:
yarn add -D @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-react-query
# or
npm install -D @graphql-codegen/typescript-operations @graphql-codegen/typescript @graphql-codegen/typescript-react-query
Quite a lot of plugins, let me explain the purpose of each plugin:
@graphql-codegen/typescript-operations
– this plugin generates the TypeScript types for the Queries, Mutations, Subscriptions, and Fragments that are only in use.@graphql-codegen/typescript
– this plugin generates the base TypeScript types, depending on the structure of the GraphQL schema.@graphql-codegen/typescript-react-query
– this plugin generates typed hooks for the various GraphQL operations.
Out-of-the-box, the GraphQL CodeGen CLI relies on a configuration file, codegen.yml
, codegen.js
or codegen.json
to manage all possible options.
In the root directory, create a codegen.yml
file and add the following configurations needed by the GraphQL CodeGen CLI.
codegen.yml
schema: http://localhost:3000/api/graphql
documents: './client/**/*.graphql'
generates:
./client/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-query
config:
fetcher: graphql-request
schema
– the path to a schema file or the URL of a GraphQL endpoint.documents
– an array of paths indicating the locations of the GraphQL files.generates
– indicates the destination to output the generated code.plugins
– list the plugins needed by CodeGenfetcher
– the asynchronous data fetching client
Now add the script below to the package.json
file.
package.json
{
"scripts": {
"generate": "graphql-codegen --config codegen.yml"
}
}
Creating the GraphQL Mutations and Queries
Now that we’ve configured GraphQL Code Generator, let’s add the queries and mutations to the client/graphql
folder.
Create Post Mutation
client/graphql/CreatePostMutation.graphql
mutation CreatePost($input: PostInput!) {
createPost(input: $input) {
status
post {
id
title
content
category
user
image
createdAt
updatedAt
}
}
}
Update Post Mutation
client/graphql/UpdatePostMutation.graphql
mutation UpdatePost($input: UpdatePostInput!, $updatePostId: String!) {
updatePost(input: $input, id: $updatePostId) {
status
post {
id
title
content
category
image
createdAt
updatedAt
}
}
}
Delete Post Mutation
client/graphql/DeletePostMutation.graphql
mutation DeletePost($deletePostId: String!) {
deletePost(id: $deletePostId)
}
Get a Single Post Query
client/graphql/GetPostQuery.graphql
query GetAllPosts($input: PostFilter!) {
getPosts(input: $input) {
status
results
posts {
id
_id
id
title
content
category
user {
email
name
photo
}
image
createdAt
updatedAt
}
}
}
Get All Post Query
client/graphql/GetAllPostsQuery.graphql
query GetAllPosts($input: PostFilter!) {
getPosts(input: $input) {
status
results
posts {
id
_id
id
title
content
category
user {
email
name
photo
}
image
createdAt
updatedAt
}
}
}
Generating the React Query Hooks with CodeGen
Since we have defined the mutations and queries, let’s execute the generate script we included in the package.json
file.
yarn generate
# or
npm run generate
After CodeGen has generated the code, you should see a newly-created ./client/generated/graphql.ts
file having generated TypesScript types, and React Query hooks.
Create Reusable Components with tailwindCss
Creating the Modal Component
Modals are very useful for collecting user information, providing updates, or encouraging users to take specific actions.
We are going to use React Portals to display the modal. React Portals allow us to render a component outside the DOM hierarchy of the parent component to avoid compromising the parent-child relationship between components.
Now let’s use the createPortal
function provided by react-dom
in conjunction with tailwindCss to create a reusable modal component.
client/components/modals/post.modal.tsx
import ReactDom from 'react-dom';
import React, { FC } from 'react';
type IPostModal = {
openPostModal: boolean;
setOpenPostModal: (openPostModal: boolean) => void;
children: React.ReactNode;
};
const PostModal: FC<IPostModal> = ({
openPostModal,
setOpenPostModal,
children,
}) => {
if (!openPostModal) return null;
return ReactDom.createPortal(
<>
<div
className='fixed inset-0 bg-[rgba(0,0,0,.5)] z-[1000]'
onClick={() => setOpenPostModal(false)}
></div>
<div className='max-w-lg w-full rounded-md fixed top-[15%] left-1/2 -translate-x-1/2 bg-white z-[1001] p-6'>
{children}
</div>
</>,
document.getElementById('post-modal') as HTMLElement
);
};
export default PostModal;
Next, let’s create a Next.js _document.tsx
page and add a Div with a post-modal
ID attribute. This will make React to render the modal outside the DOM hierarchy but within the <div id='post-modal'></div>
element.
pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() {
return (
<Html>
<Head>
<link
href='https://unpkg.com/boxicons@2.1.2/css/boxicons.min.css'
rel='stylesheet'
></link>
</Head>
<body className='font-Poppins'>
<Main />
<NextScript />
<div id='post-modal'></div>
</body>
</Html>
);
}
Creating the Message Component
client/components/Message.tsx
import React, { FC } from 'react';
type IMessageProps = {
children: React.ReactNode;
};
const Message: FC<IMessageProps> = ({ children }) => {
return (
<div
className='max-w-3xl mx-auto rounded-lg px-4 py-3 shadow-md bg-teal-100 flex items-center justify-center h-40'
role='alert'
>
<span className='text-teal-500 text-xl font-semibold'>{children}</span>
</div>
);
};
export default Message;
Creating a Custom Input Field with React-Hook-Form
client/components/TextInput.tsx
import React from 'react';
import { useFormContext } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
type TextInputProps = {
label: string;
name: string;
type?: string;
};
const TextInput: React.FC<TextInputProps> = ({
label,
name,
type = 'text',
}) => {
const {
register,
formState: { errors },
} = useFormContext();
return (
<div className='mb-2'>
<label className='block text-gray-700 text-lg mb-2' htmlFor='title'>
{label}
</label>
<input
className={twMerge(
`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none`,
`${errors[name] && 'border-red-500'}`
)}
type={type}
{...register(name)}
/>
<p
className={twMerge(
`text-red-500 text-xs italic mb-2 invisible`,
`${errors[name] && 'visible'}`
)}
>
{errors[name]?.message as string}
</p>
</div>
);
};
export default TextInput;
GraphQL Request and React Query Clients
Create a client/requests/graphqlRequestClient.ts
file and add the following code to create the React Query and GraphQL request clients.
client/requests/graphqlRequestClient.ts
import { GraphQLClient } from 'graphql-request';
import { QueryClient } from 'react-query';
const GRAPHQL_ENDPOINT = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT as string;
const graphqlRequestClient = new GraphQLClient(GRAPHQL_ENDPOINT, {
credentials: 'include',
mode: 'cors',
});
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 1000,
},
},
});
export default graphqlRequestClient;
React Query & GraphQL Request Create Mutation
The CreatePost
component contains a form built with the React-Hook-Form library that contains the fields required to create a new post.
The form validation rules are defined with the Zod schema validation library and passed to the React-Hook-Form useForm()
method via the zodResolver()
function. You can read more about the Zod library from https://github.com/colinhacks/zod.
The useForm()
hook function provided by React-Hook-Form returns an object containing methods and properties from handling the form submission to displaying the errors.
client/components/posts/create.post.tsx
import React, { FC, useEffect } from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import { object, string, TypeOf } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FileUpLoader from '../FileUpload';
import { LoadingButton } from '../LoadingButton';
import TextInput from '../TextInput';
import { useCreatePostMutation } from '../../generated/graphql';
import graphqlRequestClient, {
queryClient,
} from '../../requests/graphqlRequestClient';
import { toast } from 'react-toastify';
import useStore from '../../store';
const createPostSchema = object({
title: string().min(1, 'Title is required'),
category: string().min(1, 'Category is required'),
content: string().min(1, 'Content is required'),
image: string().min(1, 'Image is required'),
});
type CreatePostInput = TypeOf<typeof createPostSchema>;
type ICreatePostProp = {
setOpenPostModal: (openPostModal: boolean) => void;
};
const CreatePost: FC<ICreatePostProp> = ({ setOpenPostModal }) => {
const store = useStore();
const { isLoading, mutate: createPost } = useCreatePostMutation(
graphqlRequestClient,
{
onSuccess(data) {
store.setPageLoading(false);
setOpenPostModal(false);
queryClient.refetchQueries('GetAllPosts');
toast('Post created successfully', {
type: 'success',
position: 'top-right',
});
},
onError(error: any) {
store.setPageLoading(false);
setOpenPostModal(false);
error.response.errors.forEach((err: any) => {
toast(err.message, {
type: 'error',
position: 'top-right',
});
});
},
}
);
const methods = useForm<CreatePostInput>({
resolver: zodResolver(createPostSchema),
});
const {
register,
handleSubmit,
formState: { errors },
} = methods;
useEffect(() => {
if (isLoading) {
store.setPageLoading(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
const onSubmitHandler: SubmitHandler<CreatePostInput> = async (data) => {
createPost({ input: data });
};
return (
<section>
<h2 className='text-2xl font-semibold mb-4'>Create Post</h2>
<FormProvider {...methods}>
<form className='w-full' onSubmit={handleSubmit(onSubmitHandler)}>
<TextInput name='title' label='Title' />
<TextInput name='category' label='Category' />
<div className='mb-2'>
<label className='block text-gray-700 text-lg mb-2' htmlFor='title'>
Content
</label>
<textarea
className={twMerge(
`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none`,
`${errors.content && 'border-red-500'}`
)}
rows={4}
{...register('content')}
/>
<p
className={twMerge(
`text-red-500 text-xs italic mb-2 invisible`,
`${errors.content && 'visible'}`
)}
>
{errors.content ? errors.content.message : ''}
</p>
</div>
<FileUpLoader name='image' />
<LoadingButton loading={isLoading} textColor='text-ct-blue-600'>
Create Post
</LoadingButton>
</form>
</FormProvider>
</section>
);
};
export default CreatePost;
In the above, we evoked the useCreatePostMutation()
hook generated by the GraphQL Code Generator and provided it with the GraphQL client we defined in the client/requests/graphqlRequestClient.ts
file.
Also, the React-Hook-Form handleSubmit()
function is evoked when the form is submitted. If the form is valid the create post mutation will make a request to the Next.js GraphQL API to add the new post to the database.
If the mutation resolves successfully, the queryClient.refetchQueries('GetAllPosts')
will be evoked to re-fetch all the posts from the database.
On the other hand, if the mutation resolves in error, the React-Toastify component will be evoked to display them.
React Query & GraphQL Request Update Mutation
The UpdatePost component is similar to the CreatePost component with some little tweaks.
Here we will evoke the useUpdatePostMutation()
hook generated by GraphQL Code Generator to update the post in the database.
client/components/posts/update.post.tsx
import React, { FC, useEffect } from 'react';
import { FormProvider, SubmitHandler, useForm } from 'react-hook-form';
import { twMerge } from 'tailwind-merge';
import { object, string, TypeOf } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import FileUpLoader from '../FileUpload';
import { LoadingButton } from '../LoadingButton';
import TextInput from '../TextInput';
import { useUpdatePostMutation } from '../../generated/graphql';
import graphqlRequestClient from '../../requests/graphqlRequestClient';
import { toast } from 'react-toastify';
import useStore from '../../store';
import { IPost } from '../../lib/types';
import { useQueryClient } from 'react-query';
type IUpdatePostProps = {
post: IPost;
setOpenPostModal: (openPostModal: boolean) => void;
};
const updatePostSchema = object({
title: string().min(1, 'Title is required'),
category: string().min(1, 'Category is required'),
content: string().min(1, 'Content is required'),
image: string().min(1, 'Image is required'),
});
type UpdatePostInput = TypeOf<typeof updatePostSchema>;
const UpdatePost: FC<IUpdatePostProps> = ({ post, setOpenPostModal }) => {
const store = useStore();
const queryClient = useQueryClient();
const { isLoading, mutate: updatePost } = useUpdatePostMutation(
graphqlRequestClient,
{
onSuccess(data) {
store.setPageLoading(false);
setOpenPostModal(false);
queryClient.refetchQueries('GetAllPosts');
toast('Post updated successfully', {
type: 'success',
position: 'top-right',
});
},
onError(error: any) {
store.setPageLoading(false);
setOpenPostModal(false);
error.response.errors.forEach((err: any) => {
toast(err.message, {
type: 'error',
position: 'top-right',
});
});
},
}
);
const methods = useForm<UpdatePostInput>({
resolver: zodResolver(updatePostSchema),
});
const {
register,
handleSubmit,
formState: { errors },
} = methods;
useEffect(() => {
if (isLoading) {
store.setPageLoading(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
useEffect(() => {
if (post) {
methods.reset(post);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onSubmitHandler: SubmitHandler<UpdatePostInput> = async (data) => {
updatePost({ input: data, updatePostId: post._id });
};
return (
<section>
<h2 className='text-2xl font-semibold mb-4'>Update Post</h2>
<FormProvider {...methods}>
<form className='w-full' onSubmit={handleSubmit(onSubmitHandler)}>
<TextInput name='title' label='Title' />
<TextInput name='category' label='Category' />
<div className='mb-2'>
<label className='block text-gray-700 text-lg mb-2' htmlFor='title'>
Content
</label>
<textarea
className={twMerge(
`appearance-none border border-ct-dark-200 rounded w-full py-3 px-3 text-gray-700 mb-2 leading-tight focus:outline-none`,
`${errors.content && 'border-red-500'}`
)}
rows={4}
{...register('content')}
/>
<p
className={twMerge(
`text-red-500 text-xs italic mb-2 invisible`,
`${errors.content && 'visible'}`
)}
>
{errors.content ? errors.content.message : ''}
</p>
</div>
<FileUpLoader name='image' />
<LoadingButton loading={isLoading} textColor='text-ct-blue-600'>
Update Post
</LoadingButton>
</form>
</FormProvider>
</section>
);
};
export default UpdatePost;
React Query & GraphQL Request Delete Mutation
client/components/posts/post.component.tsx
import React, { FC, useEffect, useState } from 'react';
import { format, parseISO } from 'date-fns';
import { twMerge } from 'tailwind-merge';
import Image from 'next/future/image';
import { IPost } from '../../lib/types';
import { useDeletePostMutation } from '../../generated/graphql';
import graphqlRequestClient, {
queryClient,
} from '../../requests/graphqlRequestClient';
import { toast } from 'react-toastify';
import useStore from '../../store';
import PostModal from '../modals/post.modal';
import UpdatePost from './update.post';
type PostItemProps = {
post: IPost;
};
const PostItem: FC<PostItemProps> = ({ post }) => {
const [openMenu, setOpenMenu] = useState(false);
const [openPostModal, setOpenPostModal] = useState(false);
const store = useStore();
const { isLoading, mutate: deletePost } = useDeletePostMutation(
graphqlRequestClient,
{
onSuccess(data) {
store.setPageLoading(false);
queryClient.refetchQueries('GetAllPosts');
toast('Post deleted successfully', {
type: 'success',
position: 'top-right',
});
},
onError(error: any) {
store.setPageLoading(false);
error.response.errors.forEach((err: any) => {
toast(err.message, {
type: 'error',
position: 'top-right',
});
});
},
}
);
useEffect(() => {
if (isLoading) {
store.setPageLoading(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
const toggleMenu = () => {
setOpenMenu(!openMenu);
};
const onDeleteHandler = (id: string) => {
toggleMenu();
if (window.confirm('Are you sure')) {
deletePost({ deletePostId: id });
}
};
return (
<>
<div className='rounded-md shadow-md bg-white'>
<div className='mx-2 mt-2 overflow-hidden rounded-md'>
<Image
src={post.image}
alt={post.title}
className='object-fill w-full h-full'
width={400}
height={250}
/>
</div>
<div className='p-4'>
<h5 className='font-semibold text-xl text-[#4d4d4d] mb-4'>
{post.title.length > 25
? post.title.substring(0, 25) + '...'
: post.title}
</h5>
<div className='flex items-center mt-4'>
<p className='p-1 rounded-sm mr-4 bg-[#dad8d8]'>{post.category}</p>
<p className='text-[#ffa238]'>
{format(parseISO(post.createdAt), 'PPP')}
</p>
</div>
</div>
<div className='flex justify-between items-center px-4 pb-4'>
<div className='flex items-center'>
<div className='w-12 h-12 rounded-full overflow-hidden'>
<Image
src={post.user.photo}
alt={post.user.name}
className='object-cover w-full h-full'
height={100}
width={100}
/>
</div>
<p className='ml-4 text-sm font-semibold'>{post.user.name}</p>
</div>
<div className='relative'>
<div
className='text-3xl text-[#4d4d4d] cursor-pointer p-3'
onClick={toggleMenu}
>
<i className='bx bx-dots-horizontal-rounded'></i>
</div>
<ul
className={twMerge(
`absolute bottom-5 -right-1 z-50 py-2 rounded-sm bg-white shadow-lg transition ease-out duration-300 invisible`,
`${openMenu ? 'visible' : 'invisible'}`
)}
>
<li
className='w-24 h-7 py-3 px-2 hover:bg-[#f5f5f5] flex items-center gap-2 cursor-pointer transition ease-in duration-300'
onClick={() => {
setOpenPostModal(true);
toggleMenu();
}}
>
<i className='bx bx-edit-alt'></i> <span>Edit</span>
</li>
<li
className='w-24 h-7 py-3 px-2 hover:bg-[#f5f5f5] flex items-center gap-2 cursor-pointer transition ease-in duration-300'
onClick={() => onDeleteHandler(post._id)}
>
<i className='bx bx-trash'></i> <span>Delete</span>
</li>
</ul>
</div>
</div>
</div>
<PostModal
openPostModal={openPostModal}
setOpenPostModal={setOpenPostModal}
>
<UpdatePost post={post} setOpenPostModal={setOpenPostModal} />
</PostModal>
</>
);
};
export default PostItem;
React Query & GraphQL Request Get Query
pages/index.tsx
import type { GetServerSideProps, NextPage } from 'next';
import { useEffect } from 'react';
import { dehydrate } from 'react-query';
import { toast } from 'react-toastify';
import Header from '../client/components/Header';
import Message from '../client/components/Message';
import PostItem from '../client/components/posts/post.component';
import {
GetMeDocument,
useGetAllPostsQuery,
} from '../client/generated/graphql';
import { axiosGetMe } from '../client/requests/axiosClient';
import graphqlRequestClient, {
queryClient,
} from '../client/requests/graphqlRequestClient';
import useStore from '../client/store';
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
if (req.cookies.access_token) {
await queryClient.prefetchQuery(['getMe', {}], () =>
axiosGetMe(GetMeDocument, req.cookies.access_token as string)
);
} else {
return {
redirect: {
destination: '/login',
permanent: false,
},
};
}
return {
props: {
dehydratedState: dehydrate(queryClient),
requireAuth: true,
enableAuth: true,
},
};
};
const HomePage: NextPage = () => {
const store = useStore();
const { data: posts, isLoading } = useGetAllPostsQuery(
graphqlRequestClient,
{
input: { limit: 10, page: 1 },
},
{
select: (data) => data.getPosts.posts,
onError(error: any) {
store.setPageLoading(false);
error.response.errors.forEach((err: any) => {
toast(err.message, {
type: 'error',
position: 'top-right',
});
});
},
}
);
useEffect(() => {
if (isLoading) {
store.setPageLoading(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);
return (
<>
<Header />
<section className='bg-ct-blue-600 min-h-screen py-12'>
<div>
{posts?.length === 0 ? (
<Message>There are no posts at the moment</Message>
) : (
<div className='max-w-6xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-5 px-6'>
{posts?.map((post) => (
<PostItem key={post._id} post={post} />
))}
</div>
)}
</div>
</section>
</>
);
};
export default HomePage;
Conclusion
With this Next.js, React Query, GraphQL-Request, GraphQL Code Generator, tailwindCss, and React-Hook-Form, Zod example in TypeScript, you’ve learned how to create a full-stack CRUD app with Next.js.
Next.js Full-Stack CRUD App Source Code
Check out the complete source code for:
Hey, great tutorial, just what I needed!
What do I need to put in the api/graphql route for the codegen to work?
Thanks for the positive feedback! If you want to see how everything works together, I would recommend checking out the source code that’s linked in the article.
That should give you a good idea of how to create the GraphQL server and use React Query to communicate with it.
As for Codegen, one important step is to create a
codegen.yml
file in the root directory of your project. In this file, you’ll need to specify the required plugins and the path to your GraphQL server.I hope this helps!