Advertisement
programming Learn how to use Next.js Server Actions to submit forms without API routes. A step-by-step guide with examples, validation, and fixes for common errors.

How do I correctly submit a form using Next.js Server Actions — without API routes, client fetch, or weird hydration errors?

5 Min Read Verified Content

If you’re moving from pages router or React SPA, you might be used to:

  • Writing an API route

  • Calling it with fetch

  • Updating state manually

With Server Actions, you don’t need that anymore.

You can post directly to the server function — securely — without exposing secrets to the browser.

Let’s build it cleanly.


🏗 Step 1 — Create a Simple Form

Example in /app/page.tsx:

export default function Home() { return ( <form action={saveUser}> <input name="email" type="email" placeholder="Email" required /> <button type="submit">Save</button> </form> ); }

Looks normal right?

Now here’s the magic 👇


🔐 Step 2 — Add the Server Action

Above the component:

"use server"; export async function saveUser(formData: FormData) { const email = formData.get("email"); console.log("Saving user:", email); }

Yes — that’s the whole backend.

No API route.
No client fetch.
No useEffect.
No JSON parsing.

Just a function that runs securely on the server only.


🧪 Try It

When you submit the form:

✔ page does NOT refresh fully
✔ server logs the email
✔ function runs on backend
✔ values are type-safe

Feels like magic — but predictable.


⚠️ Common Error #1 — “Server Actions only work in Server Components”

If you see this:

Error: Server Actions cannot be used in Client Components

That means you added:

"use client";

Server Actions only run in Server Components.

If you need both:

👉 move UI logic to a client component
👉 keep action in the parent server component
👉 pass the action as a prop

Example:

// Server component "use server"; export async function saveUser(formData: FormData) {} export default function Page() { return <Form saveUser={saveUser} />; }

Client component:

"use client"; export function Form({ saveUser }) { return ( <form action={saveUser}> ... </form> ); }

❌ Common Error #2 — “Actions must be async functions”

Always write:

export async function saveUser(formData: FormData) {}

Even if you don’t await anything yet.


🛡 Adding Validation (Real-World Style)

"use server"; export async function saveUser(formData: FormData) { const email = String(formData.get("email") || "").trim(); if (!email.includes("@")) { throw new Error("Invalid email"); } // Save to DB here… }

Errors will surface in dev mode — which is great.


💾 Saving to a Database (Example With Prisma)

import { db } from "@/lib/db"; export async function saveUser(formData: FormData) { const email = formData.get("email") as string; await db.user.create({ data: { email } }); }

Note:
Your DB credentials never touch the browser — because this function runs server-side only.


🔁 Redirect After Submit

import { redirect } from "next/navigation"; export async function saveUser(formData: FormData) { // save... redirect("/thanks"); }

🧠 Progressive Enhancement (The Hidden Superpower)

Even if JavaScript is disabled, the form still works — because submission falls back to plain HTML POST.

This is what old frameworks got right — and we’re finally back there 😊


🎯 When Should You Use Server Actions?

Use them for:

✔ Form submissions
✔ Data mutations
✔ Secure logic
✔ Auth handling
✔ DB writes

Avoid them for:

✖ Streaming UI state
✖ High-frequency realtime events
✖ Store-like client values


🔥 Bonus — Show Success/Errors in UI (Without Client State)

import { useFormState } from "react-dom"; async function saveUser(prev: any, form: FormData) { const email = form.get("email") as string; if (!email.includes("@")) { return { error: "Invalid email" }; } return { success: true }; } export default function Page() { const [state, action] = useFormState(saveUser, {}); return ( <form action={action}> <input name="email" /> <button>Save</button> {state?.error && <p>{state.error}</p>} {state?.success && <p>Saved!</p>} </form> ); }

No Redux.
No useState.
No API routes.

Still fully interactive.


🏁 Final Thoughts

Server Actions quietly fix a decade of frontend complexity.

Once you understand the flow:

👉 form → server function → DB → UI feedback

…your codebase becomes smaller, safer, and easier to reason about.

Advertisement
Back to Programming