Skip to main content
Chapter 4 Server Actions and Forms

Introduction to Server Actions

20 min read Lesson 13 / 28

Server Actions: Forms Without API Routes

Server Actions are async functions that run on the server, triggered from Client Components. They eliminate the need for separate API route handlers for data mutations — form submissions, database writes, and file uploads all work without a custom endpoint.

Defining a Server Action

// app/actions/posts.ts
'use server'; // Everything in this file runs on the server

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function createPost(formData: FormData) {
    const title = formData.get('title') as string;
    const content = formData.get('content') as string;
    const slug = title.toLowerCase().replace(/\s+/g, '-');

    if (!title || !content) {
        throw new Error('Title and content are required');
    }

    await db.post.create({
        data: { title, content, slug },
    });

    revalidatePath('/blog'); // Invalidate the blog list cache
    redirect('/blog');       // Redirect after creation
}

Using a Server Action in a Form

// app/blog/new/page.tsx (Server Component — no 'use client' needed)
import { createPost } from '@/actions/posts';

export default function NewPostPage() {
    return (
        <form action={createPost}>
            <input
                name="title"
                placeholder="Post title"
                className="w-full border p-2 rounded"
                required
            />
            <textarea
                name="content"
                placeholder="Write your post..."
                className="w-full border p-2 rounded mt-4"
                rows={10}
                required
            />
            <button
                type="submit"
                className="mt-4 px-6 py-2 bg-blue-600 text-white rounded"
            >
                Publish
            </button>
        </form>
    );
}

Inline Server Actions

For simple cases, define the action inline in the Server Component:

// app/todos/page.tsx
export default async function TodosPage() {
    const todos = await getTodos();

    async function addTodo(formData: FormData) {
        'use server'; // Inline directive
        const text = formData.get('text') as string;
        await db.todo.create({ data: { text } });
        revalidatePath('/todos');
    }

    return (
        <div>
            <form action={addTodo}>
                <input name="text" placeholder="New todo" required />
                <button type="submit">Add</button>
            </form>
            <ul>
                {todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
            </ul>
        </div>
    );
}

Server Actions work without JavaScript enabled in the browser — they degrade gracefully to standard HTML form submissions.