Articles

Next.js Server Actions, Explained

Server Actions let Next.js run server-side functions directly from client components and forms, without hand-written API routes. How they work.

Takina Takina · · 4 min read
Abstract illustration representing a frontend web application

Server Actions are async functions that run only on the server but can be called directly from a React component, including from a <form>’s action attribute, without you writing a separate API route to handle the request. Next.js compiles each Server Action into its own endpoint behind the scenes and generates the fetch call needed to invoke it, so the code you write reads like a normal function call even though it crosses a network boundary.

They build on React Server Components: where Server Components let you render on the server, Server Actions let you mutate data on the server, closing the loop for building a page that reads and writes without a hand-rolled REST or GraphQL layer in between.

Declaring a Server Action

A function becomes a Server Action with the "use server" directive, either at the top of the function or at the top of a whole module:

// app/actions.ts
"use server";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  await db.posts.insert({ title });
}

Used directly on a form, no client-side JavaScript or onSubmit handler is required:

import { createPost } from "./actions";

export default function NewPostForm() {
  return (
    <form action={createPost}>
      <input name="title" />
      <button type="submit">Create</button>
    </form>
  );
}

Submitting this form works even before any client-side JavaScript has loaded — the browser’s native form submission triggers it, and Next.js progressively enhances it once the JavaScript bundle arrives. That’s a meaningful difference from a typical fetch()-based form handler, which does nothing until the client bundle is running.

Calling a Server Action from an event handler

Server Actions aren’t limited to <form action>. They can be called like any async function from a client component’s event handler, which is the more familiar path if you’re migrating from a fetch()-to-an-API-route pattern:

"use client";
import { deletePost } from "./actions";

export function DeleteButton({ id }: { id: string }) {
  return (
    <button onClick={() => deletePost(id)}>
      Delete
    </button>
  );
}

Under the hood this still makes a network request — Server Actions don’t eliminate the client-server boundary, they just remove the boilerplate of defining a route handler, wiring up fetch, and keeping the two in sync.

Server Actions vs API routes

Server ActionsAPI routes
BoilerplateOne function, no route fileRoute handler plus a client fetch call
Progressive enhancementBuilt in via <form action>Requires manual work
Consumable by external clientsNo — tied to the appYes — a stable HTTP endpoint
Type safety across the callEnd-to-end, same TypeScript typesOnly if you maintain shared types manually
Best forMutations from within the app’s own UIPublic or third-party-consumed APIs

The practical rule: if the only caller is your own app’s UI, a Server Action usually removes a layer of indirection. If you need a stable endpoint that a mobile app, a webhook, or a third party will call, that’s still a job for a conventional API route — Server Actions are compiled with internal, action-specific IDs and aren’t meant to be a public contract in the way a REST endpoint is.

Revalidating data after a mutation

A Server Action that changes data usually needs to tell Next.js which cached data is now stale. revalidatePath and revalidateTag handle that:

"use server";
import { revalidatePath } from "next/cache";

export async function createPost(formData: FormData) {
  const title = formData.get("title") as string;
  await db.posts.insert({ title });
  revalidatePath("/posts");
}

Without this, a page using cached data from before the mutation could keep showing stale results after the action completes. This is the same caching layer used by fetch() calls in Server Components, so revalidation ties the two together — a mutation invalidates exactly the cache entries the next render needs to be correct.

Error handling and pending states

Server Actions integrate with React’s useActionState and useFormStatus hooks so a form can show a pending spinner or validation errors without manually tracking a loading boolean:

"use client";
import { useFormStatus } from "react-dom";

function SubmitButton() {
  const { pending } = useFormStatus();
  return <button disabled={pending}>{pending ? "Saving…" : "Save"}</button>;
}

Errors thrown inside a Server Action propagate to the nearest error boundary by default, or can be caught and returned as structured state via useActionState when you want inline form validation messages instead of a full error page.

Security considerations

Because a Server Action compiles to a real network-reachable endpoint, it’s still subject to the same input validation and authorization checks any server-side handler needs — never trust that a Server Action was only called from the form you attached it to. Treat the formData or arguments it receives the same way you’d treat a request body on a conventional REST API: validate types, check permissions, and don’t assume client-side constraints (like a disabled button) were actually enforced.

The takeaway

Server Actions collapse the client-fetch-to-API-route pattern into a single async function marked "use server", callable directly from a form or event handler with progressive enhancement built in. They pair naturally with Server Components for reads and revalidatePath/revalidateTag for keeping the cache correct after a write — but for endpoints meant to be called from outside your own app’s UI, a conventional API route is still the right tool.

Takina Takina · · 4 min read

React Compiler: Automatic Memoization Explained

The React Compiler automatically memoizes components and values at build time, cutting most manual useMemo, useCallback, and React.memo calls.

#React #JavaScript #Web Development
Takina Takina · · 4 min read

useMemo vs useCallback: When to Use Each in React

useMemo caches a computed value; useCallback caches a function reference. Both skip work on re-render, but they memoize different things.

#JavaScript #React #Web Development