DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Last call! Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Build a Pokedex React App with a Slash GraphQL Backend
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • How to Build Slack App for Audit Requests
  • Idempotency in Distributed Systems: When and Why It Matters

Trending

  • Beyond Code Coverage: A Risk-Driven Revolution in Software Testing With Machine Learning
  • Agile and Quality Engineering: A Holistic Perspective
  • How To Develop a Truly Performant Mobile Application in 2025: A Case for Android
  • How Trustworthy Is Big Data?
  1. DZone
  2. Coding
  3. JavaScript
  4. Perfecting CRUD Functionality in NextJS

Perfecting CRUD Functionality in NextJS

CRUD operations are essential for data management. Learn how to manage data processing, React Query, pagination, and more efficiently with insights and examples.

By 
Olena Vlasenko user avatar
Olena Vlasenko
·
Rodion Salnik user avatar
Rodion Salnik
·
Feb. 11, 25 · Tutorial
Likes (4)
Comment
Save
Tweet
Share
4.7K Views

Join the DZone community and get the full member experience.

Join For Free

CRUD operations are fundamental building blocks and crucial for managing data. They are ubiquitous in virtually every application, from simple websites to complex enterprise solutions. 

NestJS Boilerplate users have already been able to evaluate and use a powerful new tool — CLI, which allows you to automatically create resources and their properties. With this tool, you can make all CRUD operations and add the necessary fields to them without writing a single line of code manually. Let’s now explore CRUD operations from the frontend perspective.

In Next.js, a React framework with server-side rendering capabilities, these operations can be efficiently managed with features that enhance performance, SEO, and developer experience. Previously, we published an article about an effective way to start a NextJS project, and now we want to go further and analyze the details and nuances of working with the APIs in Next.js.

As we know, the acronym CRUD stands for Create, Read, Update, and Delete. This concept represents the fundamental operations that can be performed on any data. Let's consider working with CRUD operations using the example of the administrative panel user, where functionalities like adding, editing, and deleting users are implemented, along with retrieving information about them. 

The custom React hooks discussed below, handling data processing in React Query, pagination, error management, and more, are already integrated into the Extensive-React-Boilerplate. Naturally, you can leverage this boilerplate directly. In the following sections, we’ll share our insights on implementing these features. 

Create Operation

Use Case

Submitting data to create a new resource (e.g., user registration, adding a new product).

Implementation

Collect data from the form, send a POST request to the server, handle the response, and update the UI accordingly.

Let’s observe an example. Making a POST request to the API is incorporated, creating a new user. In the snippet below, the usePostUserService hook is used to encapsulate this logic. We’ve specified the data structure for creating a new user by defining the request and response types, but omit this part here to help you focus. You can see more detailed information or a more complete picture in the repository Extensive-React-Boilerplate because this and all the following code snippets are from there.

So, we’ll create a custom hook usePostUserService that uses the useFetch hook to send a POST request. It takes user data as input and sends it to the API:

TypeScript
 
function usePostUserService() {
  const fetch = useFetch();
  return useCallback(
    (data: UserPostRequest, requestConfig?: RequestConfigType) => {
      return fetch(`${API_URL}/v1/users`, {
        method: "POST",
        body: JSON.stringify(data),
        ...requestConfig,
      }).then(wrapperFetchJsonResponse<UserPostResponse>);
    },
    [fetch]
  );
}


The function wrapperFetchJsonResponse will be examined later in this article when we get to "error handling."

Read Operations

Use Case

Fetching and displaying a list of resources or a single resource (e.g., fetching user profiles and product lists).

Implementation

Send a GET request to fetch data, handle loading and error states, and render the data in the UI.

In our example, reading data involves making GET requests to the API to fetch user data. It can include fetching all users with pagination, filters, and sorting or fetching a single user by ID after defining the request (UsersRequest) and response types (UsersResponse). 

To fetch all users in the custom  useGetUsersService hook, we send a GET request with query parameters for pagination, filters, and sorting:

TypeScript
 
function useGetUsersService() {
  const fetch = useFetch();
  return useCallback(
    (data: UsersRequest, requestConfig?: RequestConfigType) => {
      const requestUrl = new URL(`${API_URL}/v1/users`);
      requestUrl.searchParams.append("page", data.page.toString());
      requestUrl.searchParams.append("limit", data.limit.toString());
      if (data.filters) {
        requestUrl.searchParams.append("filters", JSON.stringify(data.filters));
      }
      if (data.sort) {
        requestUrl.searchParams.append("sort", JSON.stringify(data.sort));
      }
      return fetch(requestUrl, {
        method: "GET",
        ...requestConfig,
      }).then(wrapperFetchJsonResponse<UsersResponse>);
    },
    [fetch]
  );
}


For fetching a single user, the useGetUserService hook sends a GET request to fetch a user by ID:

TypeScript
 
function useGetUserService() {
  const fetch = useFetch();
  return useCallback(
    (data: UserRequest, requestConfig?: RequestConfigType) => {
      return fetch(`${API_URL}/v1/users/${data.id}`, {
        method: "GET",
        ...requestConfig,
      }).then(wrapperFetchJsonResponse<UserResponse>);
    },
    [fetch]
  );
}


Update Operation

Use Case

Editing an existing resource (e.g., updating user information, editing a blog post).

Implementation

Collect updated data, send a PUT or PATCH request to the server, handle the response, and update the UI.

Let’s carry out updating an existing user, which involves sending a PATCH request to the API with the updated user data. For this, in the custom  usePatchUserService hook, we send a PATCH request with the user ID and updated data after defining the request UserPatchRequest and response types UserPatchResponse: 

TypeScript
 
function usePatchUserService() {
  const fetch = useFetch();
  return useCallback(
    (data: UserPatchRequest, requestConfig?: RequestConfigType) => {
      return fetch(`${API_URL}/v1/users/${data.id}`, {
        method: "PATCH",
        body: JSON.stringify(data.data),
        ...requestConfig,
      }).then(wrapperFetchJsonResponse<UserPatchResponse>);
    },
    [fetch]
  );
}


Note: Using PATCH instead of PUT is more advanced for partial data updates, while PUT is typically used for full resource updates.

Delete Operation

Use Case

Removing a resource (e.g., deleting a user or removing an item from a list).

Implementation

Send a DELETE request to the server, handle the response, and update the UI to reflect the removal.

In our next example, deleting a user involves sending a DELETE request to your API with the user ID. After defining the request (UsersDeleteRequest) and response types (UsersDeleteResponse) in the useDeleteUsersService hook, a DELETE request is transmitted to remove the user by ID.

TypeScript
 
function useDeleteUsersService() {
  const fetch = useFetch();
  return useCallback(
    (data: UsersDeleteRequest, requestConfig?: RequestConfigType) => {
      return fetch(`${API_URL}/v1/users/${data.id}`, {
        method: "DELETE",
        ...requestConfig,
      }).then(wrapperFetchJsonResponse<UsersDeleteResponse>);
    },
    [fetch]
  );
}


These hooks abstract the complexity of making HTTP requests and handling responses. Using such an approach ensures a clean and maintainable codebase, as the data-fetching logic is encapsulated and reusable across your components. 

Retrieving Data in Next.js

Ok, we have dealt with examples of processing CRUD operations, and let's take a closer look at the methods of obtaining data offered by Next.js because it, as a framework, adds its functions and optimizations over React. It is clear that Next.js, beyond CSR (client-side rendering), provides advanced features like SSR (server-side rendering), SSG (static site generation), built-in API routes, and hybrid rendering. So, let's discuss commonalities and differences in retrieving data in Next.js and React.

As soon as React apps are purely client-side, so data fetching happens on the client after the initial page load. For dynamic pages that need to fetch data every time a page is loaded, it is more suitable to use SSR. In this case, data is fetched on the server at the request time. 

In the case of SSG, which is suitable for static pages where data doesn’t change often, data is fetched at build time. So, the getStaticProps method helps us to fetch data at build time (SSG). If we need pages to be pre-render based on dynamic routes and the data fetched at build time, the getStaticPaths method is allowing to do this. It is used in conjunction with the getStaticProps to generate dynamic routes at build time. It should be noted that starting with Next 14, we can make requests directly in components without these methods, which gives a more "React experience."

Client-side data fetching with useQuery can be used for interactive components that need to fetch data on the client side, with the initial state hydrated from server-side fetched data. For fetching data that changes frequently or for adding client-side interactivity, the 'useSWR' strategy is useful. It’s a React hook for client-side data fetching with caching and revalidation. It allows fetching data on the client side, usually after the initial page load. Nevertheless, it does not fetch data at build time or on the server for SSR, but it can revalidate and fetch new data when required. 

To summarize the information about the methods above, we can take a look at the table that provides a comprehensive overview of the different data fetching methods in Next.js, highlighting their respective timings and use cases.

Method Data fetching timing use case
getStaticPaths Static site generation (SSG) At build time Pre-render pages for dynamic routes based on data available at build time.
getStaticProps Static site generation (SSG)
At build time Pre-render pages with static content at build time. Ideal for content that doesn't change frequently.
getServerSideProps Server-side rendering (SSR) On each request Fetch data on the server for each request, providing up-to-date content. Ideal for dynamic content that changes frequently.
useQuery Client-side rendering (CSR) After the initial page load Fetch initial data server-side, hydrate, reduce redundant network requests, background refetching.
useSWR Client-side rendering (CSR) After the initial page load Fetch and revalidate data on the client side, suitable for frequently changing data.


Using React Query With Next.js

React Query provides hooks for fetching, caching, synchronizing, and updating server-state, making it a great tool for handling data in both React and Next.js applications. Key benefits of its use are:

  • Efficient data fetching. It handles caching and background data synchronization, reducing redundant network requests.
  • Automatic refetching. Data can be automatically refetched in the background when it becomes stale, ensuring that the UI always displays the latest information.
  • Integrated error handling. Built-in support for handling errors and retries, making it easier to manage network failures and server errors.
  • Optimistic updates. The useMutation hook provides optimistic updates by providing an easy way to handle both the optimistic UI changes and rollback logic if the server request fails.
  • Ease of integration with Next.js. It can be seamlessly integrated with other Next.js data fetching methods like getStaticProps or getServerSideProps (if needed).
  • Inspection of query and mutation. The ReactQueryDevtools tool provides the possibility of viewing the status, data, errors, and other details of all active queries and mutations and watching the query states update in real time as your application runs.

QueryClientProvider

QueryClientProvider is a context provider component that supplies a QueryClient instance to the React component tree. This instance is necessary for using hooks like useQuery.  To set it up, it needs to be placed at the root of your component tree and configure global settings for queries and mutations like retry behavior, cache time, and more. After this, it initializes the React Query client and makes it available throughout the application.

TypeScript
 
import ReactQueryDevtools from "@/services/react-query/react-query-devtools";
...
export default function RootLayout({
...
}) {
 return (
   <html lang={language} dir={dir(language)}>
     <body>
       <InitColorSchemeScript ></InitColorSchemeScript>
       <QueryClientProvider client={queryClient}>
         <ReactQueryDevtools initialIsOpen={false} ></ReactQueryDevtools>
         ...
       </QueryClientProvider>
     </body>
   </html>
 );
}


So, why should it be added to the project? It is beneficial for:

  • Centralized configuration for all queries and mutations
  • Easy to set up and integrate into existing React applications
  • Enables features like caching, background refetching, and query invalidation

React Query Devtools

The other important feature provided by React Query is ReactQueryDevtools — a development tool for inspecting and debugging React Query states. It can be easily added to your application and accessed via a browser extension or as a component like in the example before.

During development, React Query Devtools can be used for inspection of individual queries and mutations, understanding why certain queries are prefetching, monitoring the state of the query cache, and seeing how it evolves.

Pagination and Infinite Scrolling

To implement pagination controls or infinite scrolling using features in libraries, useInfiniteQuery is a perfect fit. First, we generate unique keys for caching and retrieving queries in React Query. The by method here creates a unique key based on the sorting and filtering options.

TypeScript
 
const usersQueryKeys = createQueryKeys(["users"], {
 list: () => ({
   key: [],
   sub: {
     by: ({
       sort,
       filter,
     }: {
       filter: UserFilterType | undefined;
       sort?: UserSortType | undefined;
     }) => ({
       key: [sort, filter],
     }),
   },
 }),
});


To do this, we will use the useInfiniteQuery function from React Query and take the useGetUsersService hook discussed above in the Read Operations section.

TypeScript
 
export const useUserListQuery = ({
 sort,
 filter,
}: {
 filter?: UserFilterType | undefined;
 sort?: UserSortType | undefined;
} = {}) => {
 const fetch = useGetUsersService();
 const query = useInfiniteQuery({
   queryKey: usersQueryKeys.list().sub.by({ sort, filter }).key,
   initialPageParam: 1,
   queryFn: async ({ pageParam, signal }) => {
     const { status, data } = await fetch(
       {
         page: pageParam,
         limit: 10,
         filters: filter,
         sort: sort ? [sort] : undefined,
       },
       {
         signal,
       }
     );
     if (status === HTTP_CODES_ENUM.OK) {
       return {
         data: data.data,
         nextPage: data.hasNextPage ? pageParam + 1 : undefined,
       };
     }
   },
   getNextPageParam: (lastPage) => {
     return lastPage?.nextPage;
   },
   gcTime: 0,
 });
 return query;
};


The QueryFn here retrieves the user data based on the current page, filter, and sort parameters, and the getNextPageParam function determines the next page to fetch based on the response of the last page. When the user scrolls or requests more data, useInfiniteQuery automatically retrieves the next set of data based on the nextPage parameter — this is how infinite scrolling happens. The cache time for the query is set by the gcTime parameter.

Overall, React Query provides a comprehensive solution for managing and debugging server-state in React applications. QueryClientProvider ensures a centralized and consistent configuration for all queries and mutations, while ReactQueryDevtools offers powerful tools for inspecting and understanding query behavior during development. 

Error Handling

Implementing CRUD operations always requires proper error handling to ensure user-friendliness and application reliability. Server errors are usually associated with failed processing of a client request, errors in server code, resource overload, infrastructure misconfiguration, or failures in external services. For error handling,  Extensive-React-Boilerplate suggests using the wrapperFetchJsonResponse function:

TypeScript
 
async function wrapperFetchJsonResponse<T>(
 response: Response
): Promise<FetchJsonResponse<T>> {
 const status = response.status as FetchJsonResponse<T>["status"];
 return {
   status,
   data: [
     HTTP_CODES_ENUM.NO_CONTENT,
     HTTP_CODES_ENUM.SERVICE_UNAVAILABLE,
     HTTP_CODES_ENUM.INTERNAL_SERVER_ERROR,
   ].includes(status)
     ? undefined
     : await response.json(),
 };
}


Conclusion

In this article, we covered the fundamental CRUD operations and explored data retrieval techniques in NextJS. We delved into using React Query to manage state, also outlining the capabilities of QueryClientProvider and ReactQueryDevtools for debugging and optimizing data retrieval. Additionally, we discussed implementing pagination and infinite scrolling to handle large datasets and addressed error handling to make your applications more resilient and ensure a smooth user experience.

Following the examples and techniques outlined in this article will help you be well-equipped to handle CRUD operations in your NextJS projects. Alternatively, you can use our Extensive-react-boilerplate template for your project. It has a fully compatible nestjs-boilerplate backend that implements the ability to work with CRUD operations in minutes, without a single line of code using the CLI, we've covered this in more detail here and here for entity relationships. Keep experimenting, stay updated with best practices, and welcome to try this boilerplate if you find it useful.

Next.js Data (computing) React (JavaScript library) Requests

Opinions expressed by DZone contributors are their own.

Related

  • How to Build a Pokedex React App with a Slash GraphQL Backend
  • The Cypress Edge: Next-Level Testing Strategies for React Developers
  • How to Build Slack App for Audit Requests
  • Idempotency in Distributed Systems: When and Why It Matters

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!