Fetching data in Next.js. Server actions vs server functions vs API Routes
11/06/2025 - Raúl Cano
There are many ways to fetch data in Next.js. What Next.js recommends and what is my favorite way to do it.
Summary
Feature | Server Actions | Server Functions | API Routes |
---|---|---|---|
Cache data by default | ❌ | ❌ | ❌ |
Secure by default | ❌ | ❌ | ❌ |
Run in parallel | ❌ | ✅ | ✅ |
Public endpoints | ❌ | ❌ | ✅ |
Request method | POST | GET | GET |
Client-side execution | ✅ | ❌ | ✅ |
Server-side execution | ✅ | ✅ | ✅ |
React-query or SWR | ✅ | ❌* | ✅ |
Clean code | ✅ | ✅ | ❌* |
React-query or SWR:
Means that you can use libraries like react-query or SWR to cache the data in the client side, among other things. You can use the cache() function in the server side, for the server functions, but you won’t be able to use it in the client side, and it doesn’t have as many features as react-query or SWR.
Clean code:
If I choose to use API Routes, I would use tRPC to centralize the logic and have a cleaner codebase. But I would lose the public endpoints.
Why fetching data with server actions is not the right way
Next.js introduced server actions in version 14. And until today, in my opinion, even though I love them, it seems not even the Vercel team knows exactly the best way to use them. It looks like an experiment (a good one, but still an experiment) to see how developers will use them.
The point is that all server actions are POST requests. They were mainly designed for mutation operations, but you can still fetch data from a POST request, but you won’t be able to cache this data.
I dug deeper into this while building dopost.co. The problem was that after visiting the same page you visited before, you could see another fetch to get the same data, which means the UX experience is slower, not scalable, and causes high consumption of resources.
And at the same time they won’t run in parallel, so forget about fetching several data sources expecting to get a faster response.
However, I realized I could use SWR
or react-query
to cache this data gathered by the server action since server actions can be used on the client side and those libraries also run on the client side. But still, we’re still fetching the data sequentially and with a POST request.
After reading some Next.js docs, watching some videos plus some threads on Reddit and Peerlist, I found out the following:
The approach for fetching data in Next.js that Next.js wants us to follow is using server functions in the server components and passing it through the props to the client components. No fetching with server actions. No fetching with API Routes.
//page.tsx
import { getPosts } from "@/server/queries/getPosts"; // Server function
export default function Home() {
const posts = getPosts();
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
);
}
//server/queries/getPosts.ts
// NO "use server" here <---------
import { cache } from "react";
import { db } from "@/server/db";
export const getPosts = cache(async () => {
const posts = await db.query.posts.findMany();
return posts;
});
Basically they want you to always process the data on the server side, fetching it on the server then passing it to the browser.
My opinion
Even though this is the approach recommended by Next.js, I don’t really like it. Also, the fact of having a framework with different approaches to achieve the same goal depending on the Next.js version is not a good thing and doesn’t make me trust the framework. Don’t get me wrong, I love Next.js you can ship projects super fast and it’s not difficult to learn, plus you can have all the backend logic in the same place, which I think is perfect for an MVP.
If you ask me, I would use tRPC to handle all the fetching and mutation logic in order to centralize the logic and have a single source of truth. Otherwise you’d be dealing with server actions for the mutations and server functions plus API Routes for the fetching. And yeah, I said tRPC, because in my opinion the way API routes look in your code looks very messy and ugly when it grows, since you have to create one folder for each API.
And this goes to another point: should I use Next.js since I’m avoiding their principles? Should I just use React?
Well, I’m still having this doubt, but the fact of not needing to create a separate backend and how easy it is to handle routes in Next.js is a big plus.