Next.js (App Router) + Server Actions: Abandon API Route and move towards development that dissolves “boundaries”

Next.js (App Router) + Server Actions: Abandon API Route and move towards development that dissolves “boundaries”

"What was all that time spent writing API Route all about?"

When I touched Server Actions for the first time and felt the boundary between front and back disappear, that feeling of emptiness crossed my mind. The pain of sharing type definitions, hitting endpoints, fighting loading conditions, and messing around with caches with SWR and React Query.

Let's start with the conclusion. Mass-producing API Routes with the current Next.js is just a ``hardship'' unless you have a very special microservice configuration. Server Actions is more than just a convenience feature. This is a paradigm shift in which the front end and back end are sublimated into a single function call.


1. Why has API Route become a “relic of the past”?

I want you to remember the old development flow.
1. Create an endpoint in `pages/api`
2. Write `fetch` and define an interface to match types
3. Use `useEffect` and libraries on the client side to manage the request lifecycle

All of this was a ritual to forcibly solve the physical constraints of "exchanging data over a network" in code. Server Actions abstracts this awareness of being across the network and replaces it with simply "execution of a function."

Implementation that ensures strict type safety

By combining Zod and TypeScript, you can transparently bring backend type safety to the client. Let's abandon the laxity of using `any`.

"use server";

import { z } from "zod";
import { db } from "@/lib/db";
import { revalidatePath, revalidateTag } from "next/cache";

const UserSchema = z.object({
  username: z.string().min(3).max(20),
  email: z.string().email(),
});

// Full control over types by inferring from Zod
type UserInput = z.infer<typeof UserSchema>;

export async function createUser(prevState: unknown, formData: FormData) {
  const result = UserSchema.safeParse(Object.fromEntries(formData));
  
  if (!result.success) {
    return { error: result.error.flatten().fieldErrors };
  }

  try {
    await db.user.create({ data: result.data });
    
    // A "hands-on" method to purge the cache with specific tags
    revalidateTag("users");
    return { success: true };
  } catch (e) {
    return { error: "DB connection error" };
  }
}
`


2. Facing the “trap” of Server Actions

Server Actions are not magic. In particular, the ``caching'' and ``serialization'' limitations are the points that make many engineers want to smash their monitors.

Cache control: Uses of revalidateTag

`revalidatePath` is useful, but it loses control in complex apps. If you want to pinpoint and discard the cache only when specific data is updated, `revalidateTag` is required.
  • revalidatePath: Revalidate the cache by path (for rough updates)
  • revalidateTag: Revalidate in data cache units (more detailed control)

Know the limits of serialization

Server Actions can only handle "serializable data (JSON)" for arguments and return values. If you try to directly pass a `Date` object, `Map`, or a complex class instance, you will get a relentless error message.

The shortest way to reduce bugs is to go back to the origins of the web, which is to only exchange primitive values ​​and JSON, rather than thinking that anything can be thrown at the server.


3. Authentication wrapper as a “guardian”

Don't mistake Server Actions for simply being an "extension of components." This is definitely a public API endpoint.

Writing authentication checks in individual functions violates the DRY principle. Create a "guardian deity" using higher-order functions.

type ActionFn<T> = (user: User) => Promise<T>;

export async function withAuth<T>(action: ActionFn<T>): Promise<T> {
  const user = await getAuthUser();
  if (!user) throw new Error("Unauthorized");
  return action(user);
}

// This is all you need to use
export const updateProfile = async (data: any) => withAuth(async (user) => {
  // Safely operate the DB here
});
`


4. Reality as a professional

Server Actions are powerful, but they're not a silver bullet.

  • Small to medium-sized products: The correct answer is to use Server Actions for extremely fast development. The golden rule of team development is to introduce `eslint-plugin-react-server-components` and to protect the boundaries between `"use client" and `"use server" with static analysis.
  • Ultra-large scale/complex microservices: Independent API servers (or type sharing infrastructure such as tRPC) are still preferred. Server Actions are highly coupled, making maintenance difficult as responsibilities grow.

Finally

Mastering Server Actions means hiding the "over the network" complexity and focusing on the domain logic itself.

I would like to say this to myself back then when I was busy mass producing API Route. "Throw away all that code you've written and dissolve the boundaries." Before accumulating technical debt, I would like you to try introducing this "experience" in a small way. Beyond that, a completely new landscape of Next.js awaits.


🤖 Developer blog/public content

The developer's personal blog is also available, so please take a look. 👉 mutyan.com (Developer Blog)