Home >Web Front-end >JS Tutorial >Mastering CRUD with NextJS
In web development, 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. Meanwhile, as we have repeatedly announced, the BC Boilerplates ecosystem includes a fully compatible Extensive-React-Boilerplate to provide full functionality (which, in principle, can be a completely independent solution). 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.
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:
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".
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:
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:
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] ); }
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:
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.
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.
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] ); }
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.
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 initial state hydrated from server-side fetched data. For fetching data that changes frequently or for adding client-side interactivity it is useful the useSWR strategy. 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. |
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:
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.
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] ); }
So, why should it be added to the project? It is beneficial for:
The other important feature provided by React Query is React Query Devtools - 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 and monitoring the state of the query cache, and seeing how it evolves over time.
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.
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] ); }
To do this, we will use the useInfiniteQuery function from React Query and take the useGetUsersService hook discussed above in the Read Operations section.
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] ); }
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.
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:
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] ); }
In this article, we covered the fundamental CRUD operations, 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.
By following the examples and techniques outlined in this article, you should now 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.
Our BC Boilerplates team is always seeking ways to enhance development. We’d love to hear your thoughts on GitHub discussions or in the comments below.
Full credits for this article to Olena Vlasenko and Vlad Shchepotin ??
The above is the detailed content of Mastering CRUD with NextJS. For more information, please follow other related articles on the PHP Chinese website!