aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Web Development

Next.js Server Actions: Forms Without an API

Next.js Server Actions remove the obligation to write a separate API route just to handle a form: you put the mutation logic in a function that runs on the server and wire the form straight to it. The result is that you can create and update data without a fetch call, manual JSON serialization, or a dedicated app/api/... file. In this article I set up Server Actions from scratch on the App Router, add validation and error handling, and cover what to watch out for in production.

What is a Server Action and how does it work?

A Server Action is an async function whose body begins with the 'use server' directive. This function runs only on the server; its code is never included in the client bundle. When the form is submitted, Next.js sets up the network call between browser and server for you. You just write the function, and Next.js turns it into a callable endpoint.

There are two ways to place it: at module level in a dedicated file with 'use server' at the top, or defined inline inside a Server Component. For reusable, testable code I prefer the separate-file approach.

Write your first Server Action

Let's put the action in its own file first. The 'use server' at the top of the file turns every exported function inside it into a Server Action:

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

export async function createPost(formData: FormData) {
  const title = formData.get('title')
  const body = formData.get('body')

  // write to the database here (Prisma, Drizzle, etc.)
  await db.post.create({ data: { title, body } })

  revalidatePath('/posts')
  redirect('/posts')
}

The form side is surprisingly plain. You pass the function itself to the action attribute, not a URL:

// app/posts/new/page.tsx
import { createPost } from '@/app/actions'

export default function NewPost() {
  return (
    <form action={createPost}>
      <input name="title" />
      <textarea name="body" />
      <button type="submit">Save</button>
    </form>
  )
}

Note that the form can live inside a Server Component and works even when no JavaScript has been downloaded. That is a natural consequence of progressive enhancement.

Validate the input — never trust raw data

A Server Action is a publicly reachable endpoint; it can be called directly regardless of what you show on the client. So always validate the incoming FormData on the server. Schema-based validation with Zod is a clean approach:

'use server'
import { z } from 'zod'

const PostSchema = z.object({
  title: z.string().min(3, 'Title must be at least 3 characters'),
  body: z.string().min(10),
})

export async function createPost(prevState, formData: FormData) {
  const parsed = PostSchema.safeParse({
    title: formData.get('title'),
    body: formData.get('body'),
  })

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors }
  }

  await db.post.create({ data: parsed.data })
  revalidatePath('/posts')
  return { success: true }
}

safeParse returns a result object instead of throwing, so you can send validation errors back to the user.

Error and state handling: useActionState

To bind the errors returned by the action above to the form, we use React's useActionState hook (this is its name in React 19 / Next.js 15; in older versions it was useFormState). The hook requires a client component:

'use client'
import { useActionState } from 'react'
import { createPost } from '@/app/actions'

export function PostForm() {
  const [state, formAction, pending] = useActionState(createPost, {})

  return (
    <form action={formAction}>
      <input name="title" />
      {state?.errors?.title && <p>{state.errors.title}</p>}
      <button disabled={pending}>
        {pending ? 'Saving...' : 'Save'}
      </button>
    </form>
  )
}

The third return value, pending, is true while the action runs and is perfect for disabling the button. For more granular loading indicators you can use the useFormStatus hook in a separate button component.

Refreshing the cache: revalidatePath and revalidateTag

After a mutation the screen needs to show the current data. Because Next.js caches data aggressively, you explicitly invalidate the affected path:

  • revalidatePath('/posts') — clears the cache for a specific route.
  • revalidateTag('posts') — if you tagged your fetch calls, refreshes all data bound to that tag.
  • redirect('/posts') — sends the user to another page after the mutation; call it outside any try/catch block, because internally it throws a special error.

What to watch out for in production

  • Authorization is mandatory: An action is an endpoint. Check the session/permissions at the start of every action; hiding a button in the UI is not security.
  • Isolate side effects: Move database, email, and payment work into pure functions called by the action; keep the action a thin layer.
  • Return values must be serializable: The state returned to the client should be plain objects/arrays — no class instances or functions.
  • You may still need an API: If you have a mobile app or third-party integration, a Server Action does not replace it; you will need a real API.

Frequently Asked Questions

Are Server Actions only used with forms?

No. Binding to the action attribute is the most common path, but you can also call a Server Action from a button onClick handler or inside startTransition. Form binding is preferred because it works even when JavaScript is disabled.

Should I drop API routes entirely?

No. Server Actions are ideal for mutations triggered from your own UI. When you need webhooks, external clients, or a public REST/JSON interface, classic route handlers (app/api/...) are still the right tool.

Are Server Actions secure?

The infrastructure is secure — Next.js protects action identifiers and maps submissions through encrypted references. But the security of your business logic is on you: validate input and check authorization. Without those you are leaving an open endpoint.

Are forms getting complicated in your project because of network code? I can help you build a clean, validated, and secure data layer with Next.js Server Actions — get in touch with me.

Bu kategorideki tüm yazılar →

Devamı için