QR CookingNotes

CookingNotes

Your Personal Recipe Book

Get it on Google Play
QR FiNoteMe

FiNoteMe

Smart Finance Tracker

Get it on Google Play
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