Stop Using GET Inside Server Actions
Learn why using GET inside Next.js Server Actions creates unnecessary round trips, how to fetch data efficiently with React Server Components, and when to use Server Actions the right way โ with fun analogies and diagrams. - 8/25/2025
6 min read

๐ Stop Using GET Inside Server Actions
TL;DR: Using GET requests inside Server Actions creates unnecessary round trips and hurts performance. Use React Server Components for data fetching and Server Actions only for mutations (POST/PUT/DELETE).
If youโre diving into Next.js App Router and experimenting with Server Actions, you might be tempted to wrap every fetch in them โ including GET
requests.
But hereโs the catch: GET inside Server Actions is like calling your mom to ask if you can text her. Totally unnecessary. ๐
This guide will show you why this pattern hurts performance, when to use each approach, and how to migrate existing code for better user experience.
Letโs break it down.
โก Server Actions 101
Server Actions are special async functions that run on the server. You usually trigger them via:
- Form submissions
- Button clicks
startTransition
hooks
Theyโre serialized, sent over the wire, and executed in the server runtime.
๐ Why POST? Server Actions use POST requests for security and consistency. Unlike GET requests (which can be cached, bookmarked, or logged), POST requests ensure mutations are intentional and canโt be accidentally triggered by web crawlers or browser prefetching.
// actions.ts
"use server";
export async function addTodo(formData: FormData) {
const title = formData.get("title");
await fetch("https://api.example.com/todos", {
method: "POST",
body: JSON.stringify({ title }),
});
}
โ The GET Trap
Now imagine you try this:
"use server";
export async function getTodos() {
const res = await fetch("https://api.example.com/todos");
return res.json();
}
And then call it from your component:
'use client';
import { useState, useEffect } from 'react';
import { getTodos } from "./actions";
interface Todo {
id: string;
title: string;
completed: boolean;
}
export default function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
getTodos()
.then(setTodos)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, []);
if (loading) return <div>Loading todos...</div>;
if (error) return <div>Error: {error}</div>;
return <pre>{JSON.stringify(todos, null, 2)}</pre>;
}
๐ฅ Problem: You just created two round trips.
- Client โ Server Action (
POST
request to rungetTodos
) - Server Action โ API (
GET
request for todos)
This is slower than if you had just fetched the data directly in a React Server Component.
๐ Visualizing the Double Round Trip
Hereโs a simple diagram to show the problem:
โก Notice how the client goes through an extra middleman. Thatโs wasted time.
๐ Performance Impact
Letโs put some numbers to this problem:
Approach | Round Trips | Typical Time | Caching |
---|---|---|---|
Server Action + GET | 2 | ~400-800ms | โ No caching |
React Server Component | 1 | ~200-400ms | โ Built-in caching |
Real-world example:
- Server Action approach: 600ms (300ms clientโserver + 300ms serverโAPI)
- RSC approach: 250ms (direct serverโAPI at render time)
Thatโs a 58% performance improvement just by using the right pattern! ๐
The performance gap widens with:
- Slower networks (mobile users suffer more)
- Geographic distance (extra hop compounds latency)
- Heavy payloads (serialization overhead doubles)
โ The Right Way to Fetch Data
In React Server Components, you can fetch directly. No extra hops.
// app/todos/page.tsx
interface Todo {
id: string;
title: string;
completed: boolean;
}
export default async function TodosPage() {
const todos: Todo[] = await fetch("https://api.example.com/todos", {
cache: 'force-cache', // Cache for 1 hour
next: { revalidate: 3600 }
}).then((res) => {
if (!res.ok) throw new Error('Failed to fetch todos');
return res.json();
});
return (
<div>
<h1>My Todos</h1>
<ul>
{todos.map((todo) => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
{todo.title}
</li>
))}
</ul>
</div>
);
}
โก This happens at render time on the server, streamed to the client. Super efficient.
Key benefits:
- โ Built-in caching with Next.js fetch extensions
- โ Automatic revalidation keeps data fresh
- โ Streaming support for better perceived performance
- โ Error boundaries can catch and handle failures gracefully
๐ฏ When to Use Server Actions
-
Mutations (POST/PUT/PATCH/DELETE)
- Adding a todo
- Deleting a user
- Updating profile settings
-
Never for pure GETs (unless youโre doing something weird like combining data after a mutation).
Example mutation with UI:
// app/todos/page.tsx
import { addTodo } from "./actions";
export default function TodosPage() {
return (
<form action={addTodo}>
<input name="title" placeholder="New Todo" />
<button type="submit">Add</button>
</form>
);
}
Thatโs the sweet spot for Server Actions. ๐
๐ค Edge Cases: When GET in Server Actions Might Be OK
While rare, there are a few scenarios where GET in Server Actions could be justified:
1. Data Transformation After Mutation
"use server";
export async function createUserAndFetchDashboard(formData: FormData) {
// First, create the user (mutation)
const user = await createUser(formData);
// Then fetch their personalized dashboard data
// This GET is acceptable because it depends on the mutation result
const dashboard = await fetch(`/api/dashboard/${user.id}`);
return { user, dashboard: await dashboard.json() };
}
2. Combining Multiple Data Sources
"use server";
export async function searchAndFilter(query: string, filters: string[]) {
// Complex server-side logic that combines multiple APIs
const [searchResults, userPrefs, recommendations] = await Promise.all([
fetch(`/api/search?q=${query}`),
fetch(`/api/user-preferences`),
fetch(`/api/recommendations`)
]);
// Server-side filtering logic too complex for client
return combineAndFilter(searchResults, userPrefs, recommendations, filters);
}
3. Security-Sensitive Operations
"use server";
export async function getAdminData(userId: string) {
// Server-side permission checking before data access
const hasPermission = await checkAdminPermissions(userId);
if (!hasPermission) throw new Error('Unauthorized');
return fetch('/api/admin/sensitive-data');
}
โ ๏ธ Rule of thumb: Only use GET in Server Actions when you need server-side logic that canโt be done in RSCs.
๐ Migration Guide
Already using GET in Server Actions? Hereโs how to migrate:
Step 1: Identify GET Server Actions
# Search your codebase for the pattern
grep -r "use server" --include="*.ts" --include="*.tsx" . | xargs grep -l "fetch.*GET\|fetch.*method.*GET"
Step 2: Move to Server Components
// โ Before: GET in Server Action
"use server";
export async function getUser(id: string) {
return fetch(`/api/users/${id}`).then(res => res.json());
}
// โ
After: Direct fetch in RSC
export default async function UserProfile({ id }: { id: string }) {
const user = await fetch(`/api/users/${id}`, {
next: { revalidate: 300 } // Cache for 5 minutes
}).then(res => res.json());
return <div>{user.name}</div>;
}
Step 3: Update Client Components
// โ Before: Calling Server Action from client
'use client';
export function UserDisplay({ id }: { id: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
getUser(id).then(setUser); // Server Action call
}, [id]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}
// โ
After: Pass data from Server Component
export default async function UserPage({ id }: { id: string }) {
const user = await fetch(`/api/users/${id}`).then(res => res.json());
return <UserDisplay user={user} />; // Pass as prop
}
'use client';
export function UserDisplay({ user }: { user: User }) {
return <div>{user.name}</div>; // No loading state needed!
}
Step 4: Handle Dynamic Data
For data that changes frequently, use revalidatePath
or revalidateTag
:
// actions.ts
"use server";
import { revalidatePath } from 'next/cache';
export async function updateUser(formData: FormData) {
await updateUserInDB(formData);
revalidatePath('/profile'); // Refresh the cached data
}
๐ TL;DR
- GET โ Use React Server Components
- POST โ Use Server Actions
- Donโt wrap GETs in Server Actions unless you like extra lag ๐
๐ช Fun Analogy
Think of it like this:
- GET in RSC โ Walking straight to the fridge to grab cookies.
- GET in Server Action โ Calling your roommate to ask them to go to the fridge, grab cookies, and bring them to you. Then they call the bakery to bake cookies first. ๐
One is instant. The other? A comedy of delays.
So next time youโre tempted to use GET
in a Server Actionโฆ just walk to the fridge yourself. ๐ชโจ
๐ Related Articles
- Next.js App Router Documentation
- React Server Components Deep Dive
- Server Actions Best Practices
- Performance Optimization in Next.js
Tags: Next.js
, React
, Server Actions
, Performance
, App Router
, Server Components
, Web Development