Server Actions in Next.js 15 to manage CRUD operations
05/02/2025 - Raúl Cano
My approach to handling CRUD operations in Next.js App Route
I started building a large API layer and using TanStack Query, but then I realized it wasn’t worth it nor a good approach for the Next.js App Router and for an MVP boilerplate, because Next.js App Router already provides built-in data fetching and caching through Server Components and Server Actions.
After the Next.js 13 release, the approach to developing a Next.js application now focuses on Server Components and Server-Side Rendering (SSR), along with the introduction of Server Actions.
Server actions are server-side functions that process forms and data. They must be marked with “use server” and can be used on the server or the client side (via form actions or direct invocation).
So instead of creating multiple endpoints in our routes and managing them with TanStack, fetch requests, useEffect, etc., we will manipulate the data using server functions, which reduces client-side complexity.
That means we don’t need—or shouldn’t use—the TanStack library to avoid client-side load and should instead focus on the server, leveraging Next.js’s built-in data handling capabilities.
Here is an example of the structural approach we have taken: we have separated server actions from queries. Server actions are meant to be used in forms, while queries don’t need to be marked with "use server"
, as doing so would disable Next.js’s caching, which is crucial for performance.
src/
├── services/
│ └── users/
│ ├── mutations.ts // POST, PUT, DELETE operations with 'use server'
│ ├── queries.ts // GET operations without 'use server'
│ └── types.ts // User types
│
...
Caching clarification
Yeah, you may be thinking, “But why can’t I just include the fetch logic within the ‘use server’ directory, like server actions?
Even though you can use server actions to fetch data, Next.js discourages it because it can only send a POST request. Instead, fetch the data from the server component and pass it down as props.
— @Towel1355 on reddit
Server actions are designed for mutations (creating, updating, or deleting data), while queries (data fetching) benefit from Next.js’s built-in caching and revalidation.
Actually, if you inspect the page and check the network tab, you can see how the server actions are POST requests.
// queries.ts
export async function getUser(id: string) {
// This can be cached by Next.js
const user = await prisma.user.findUnique({ where: { id } })
return user
}
VS
// mutations.ts
"use server"
export async function getUser(id: string) {
// This bypasses Next.js cache
const user = await prisma.user.findUnique({ where: { id } })
return user
}
Note: We only will need to create API routes when we need to work with thirds services