Next.js Full-Stack App with React Query, and GraphQL-CodeGen
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 performCreate/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 makesfetching,caching,cache synchronizing, andupdating server state a breeze inReact.js/Next.js applications.
React Query is now an adaptor categorized underTanStackQuery upon the release of version4.
There are otherTanStackQuery adaptors like:
- Vue Query
- Svelte Query
- Solid Query Upcoming
I know there are other server state management libraries likeSWR,Apollo Client, andRTK Query but when you carefully analyze the benchmark for the popular server state libraries on theTanStackQuery 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 likeGraphQL-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/cliThere 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-queryQuite 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 orcodegen.json to manage all possible options.
In the root directory, create acodegen.yml file and add the following configurations needed by the GraphQL CodeGen CLI.
codegen.yml
schema: http://localhost:3000/api/graphqldocuments: './client/**/*.graphql'generates: ./client/generated/graphql.ts: plugins: - typescript - typescript-operations - typescript-react-query config: fetcher: graphql-requestschema– 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 thepackage.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 theclient/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 thepackage.json file.
yarn generate# or npm run generateAfter 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 thecreatePortal function provided byreact-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 apost-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 aclient/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
TheCreatePost 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-FormuseForm() method via thezodResolver() function. You can read more about the Zod library fromhttps://github.com/colinhacks/zod.
TheuseForm() 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 theuseCreatePostMutation() hook generated by the GraphQL Code Generator and provided it with the GraphQL client we defined in theclient/requests/graphqlRequestClient.ts file.
Also, the React-Hook-FormhandleSubmit() 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, thequeryClient.refetchQueries('GetAllPosts') will be evoked to re-fetch all the posts from the database.
On the other hand, if the mutation resolves in error, theReact-Toastify component will be evoked to display them.
React Query & GraphQL Request Update Mutation
TheUpdatePost component is similar to theCreatePost component with some little tweaks.
Here we will evoke theuseUpdatePostMutation() 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:
Build API with Python & FastAPI: SignUp User and Verify Email
CRUD RESTful API Server with Python, FastAPI, and MongoDB
2 Comments
- Jonason May 3, 2023Reply
Hey, great tutorial, just what I needed!
What do I need to put in the api/graphql route for the codegen to work? - Edemon May 3, 2023Reply
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.ymlfile 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!
Leave a ReplyCancel reply
This site uses Akismet to reduce spam.Learn how your comment data is processed.
Support Me!

Recent posts
Categories
- C#(2)
- C++(1)
- CSS / SCSS(3)
- Deno(8)
- Golang(31)
- JavaScript(5)
- NextJs(38)
- NodeJS(32)
- Programming(19)
- Python(19)
- React(38)
- Rust(35)
- Svelte(5)
- Vue(7)

