Enhancing Data Fetching in Next.js: Creating a Backend Doesn’t Get Any Better Than This
Learn a modern, type-safe way to fetch data in Next.js using React-Query and server actions without traditional API routes.

Hi there 👋,
Next.js is one of my favorite tools of all time but one feature that people really like it for which is their built-in API routes. But I don’t actually use a lot anymore the API folder has its purpose. Don’t get me wrong but there’s a much nicer way to write your backend logic in next.js that I found out like two or 3 weeks ago actually even works.
The oldest way to get your data in a React application would be define an asynchronous function with a fetch handler that makes a request to your API with any method and then attach that function right here in a button. For example as the onClick handler right there, so when we click this button that’s going to make a request and get data.
🤔 What about the loading state? right we need to keep track of the loading state separately isLoading inside of something like state, same for the error state. What about an error if anything goes wrong during the await fetch we need to save that in state as well to then show it to the user. The bottom line is fetching data like this just makes it way harder on yourself than it needs to be.
There are libraries that make it easier,
React-Query
Let’s install the react-query
Once we install that react query, it makes data fetching in our project a lot easier🚀 but it has one very big drawback, we will discuss later.
So what react-query does, it basically gives us a hook and that is called useMutation, that is basically the same thing as a fetch handle right inside of this useMutation coming from react-query.
We can now Define something called or mutation function. if you’ve never used it sounds fancy but essentially you define any function to get your data inside of here.
For example, once we mark this as asynchronous, we can use await fetch and make our API call just the same. Now you’re wondering, well Vinojan, then where’s the difference?
Well the difference is that the isLoading or isPending, how it’s named in react-query same thing, or the error property and retries and so on are all done by the library.
😪 But the big problem is that there is still no type safety, if we destructure the data, so whatever comes back from our fetch request you’re going to notice it is void or undefined. Of course in part that’s because we’re not actually returning something from the function but once we do how should react query know whatever it is the data the shape of the data that we get back.
It takes care of a lot of important stuff like error and loading states but not of one of the most important things which is four stack type safety and that’s where the way of fetching data comes in.
It’s a combination of react-query and hear me out something called server actions. server actions are a feature that the application is integrating with database directly. Here's the thing, server actions that is the beautiful thing work flawlessly together with react query and that is the most important takeaway of this.
Let’s create a new file called action.ts in anywhere, it doesn’t matter where this file is, okay!
As long as we specify use server at the very top of the file we can export any asynchronous function. Let’s call it fetchUser for example and this will run on your server.
Just say the name is ‘Vinojan’ but this works perfectly fine to use your database right here instead of an API route.
Now the beautiful thing is we can import this function on the client’s side and get full type safety instead of the fetch request or API route. Instead of all the fetch logic we have before simply import the function from our actions file and there will no error.
Now the way in which we invoke this back and function in react-query we can destructure something called mutate, let’s call it for example handleFetchUser which is a convention that I have really grown to like.
Once we invoke this function in our tsx you’re going to see something really cool because this is actually fully types safe.
If we now accept any kind of parameters in this action function, for example userId, and this userId will be the type of string that we expect right here, then you’re going to notice once we switch back to the front-end, you will actually getting a typescript error expected one to two arguments but got zero. This is completely types safe.
Now we need to pass the userId in from the front-end, so this is pretty much just the body of a POST request under the hood but abstract it in such a way where we don’t really know that.
You can destructure or check the type of data. Not only do we get type safety but also all the loading and error states are of course handled by react-query. It’s called isPending here but it’s the same thing as the loading state.
And, we can also handle events like onSuccess and onError.
I appreciate you taking the time to read this article.🙌
Before you move on to explore the next article, don’t forget to give your claps 👏 for this article and share with your friends. Stay connected with me on social media. Thanks for your support and have a great rest of your day! 🎊
✍️ Vinojan Veerapathirathasan.