Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Nextjs
  4. /Server Actions
Your Progress0%
0 of 70 completed

NextJS Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication

Understanding Server Actions

Server-side functions for form submissions and mutations

Server Actions are asynchronous functions that run on the server. They replace traditional API routes for form submissions and data mutations, providing type safety, progressive enhancement (work without JavaScript!), and seamless integration with React components. You write functions, Next.js handles the client-server communication automatically. Let's master Server Actions!

What Are Server Actions?

❌ Traditional API Routes

TYPESCRIPT
// API route: app/api/posts/route.ts
export async function POST(request: Request) {
  const data = await request.json();
  // Process data...
  return Response.json({ success: true });
}

// Client component
async function handleSubmit() {
  const response = await fetch('/api/posts', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data),
  });
}

// Problems:
// ❌ Separate API file
// ❌ Manual fetch logic
// ❌ No type safety
// ❌ Requires JavaScript
// ❌ More boilerplate

✅ Server Actions

TYPESCRIPT
// Server Action (co-located)
'use server';

async function createPost(formData: FormData) {
  const title = formData.get('title');
  // Process data...
  return { success: true };
}

// Use in component
<form action={createPost}>
  <input name="title" />
  <button>Submit</button>
</form>

// Benefits:
// ✅ Co-located with component
// ✅ Automatic communication
// ✅ Type-safe
// ✅ Works without JavaScript
// ✅ Less code

Key Benefits of Server Actions

  • Type safety: Full TypeScript support from client to server
  • Progressive enhancement: Forms work without JavaScript
  • Co-location: Define actions near components that use them
  • Automatic serialization: No manual JSON.stringify/parse
  • Integrated caching: Works with Next.js cache and revalidation
  • Streaming: Can return multiple values over time

Basic Server Action Usage

Method 1: Inline Server Action

app/page.tsx
// Server Component with inline Server Action
export default function Page() {
  async function createPost(formData: FormData) {
    'use server';
    
    const title = formData.get('title') as string;
    const content = formData.get('content') as string;
    
    // Database operation
    await db.posts.create({
      data: { title, content },
    });
    
    console.log('Post created:', title);
  }

  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

// ✅ 'use server' marks function as Server Action
// ✅ Runs on server when form submits
// ✅ Works without JavaScript
// ✅ Automatic serialization

Method 2: Separate Actions File

app/actions/posts.ts
'use server';

// All exported functions are Server Actions
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  
  await db.posts.create({
    data: { title, content },
  });
  
  return { success: true };
}

export async function deletePost(postId: string) {
  await db.posts.delete({
    where: { id: postId },
  });
  
  return { success: true };
}

export async function updatePost(postId: string, formData: FormData) {
  const title = formData.get('title') as string;
  
  await db.posts.update({
    where: { id: postId },
    data: { title },
  });
  
  return { success: true };
}

// ✅ 'use server' at top applies to entire file
// ✅ All exports are Server Actions
// ✅ Organized by domain (posts, users, etc.)
// ✅ Reusable across components
app/blog/page.tsx
import { createPost, deletePost } from '@/app/actions/posts';

export default function BlogPage() {
  return (
    <div>
      {/* Create form */}
      <form action={createPost}>
        <input name="title" />
        <button>Create</button>
      </form>

      {/* Delete form */}
      <form action={deletePost.bind(null, 'post-id-123')}>
        <button>Delete</button>
      </form>
    </div>
  );
}

// ✅ Import Server Actions
// ✅ Use directly in forms
// ✅ .bind() for passing additional arguments

Server Actions in Client Components

Calling from Client Component

app/actions/posts.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  
  await db.posts.create({
    data: { title },
  });
  
  return { success: true, message: 'Post created!' };
}
app/components/CreatePostForm.tsx
'use client';

import { createPost } from '@/app/actions/posts';
import { useState } from 'react';

export function CreatePostForm() {
  const [message, setMessage] = useState('');

  async function handleSubmit(formData: FormData) {
    const result = await createPost(formData);
    setMessage(result.message);
  }

  return (
    <form action={handleSubmit}>
      <input name="title" required />
      <button type="submit">Create</button>
      {message && <p className="text-green-600">{message}</p>}
    </form>
  );
}

// ✅ Import Server Action in Client Component
// ✅ Call like regular async function
// ✅ Handle response with state
// ✅ Full type safety

Programmatic Calling

components/DeleteButton.tsx
'use client';

import { deletePost } from '@/app/actions/posts';
import { useState } from 'react';

export function DeleteButton({ postId }: { postId: string }) {
  const [loading, setLoading] = useState(false);

  async function handleDelete() {
    if (!confirm('Delete this post?')) return;
    
    setLoading(true);
    try {
      await deletePost(postId);
      alert('Post deleted!');
    } catch (error) {
      alert('Failed to delete');
    } finally {
      setLoading(false);
    }
  }

  return (
    <button
      onClick={handleDelete}
      disabled={loading}
      className="px-4 py-2 bg-red-600 text-white rounded disabled:bg-gray-400"
    >
      {loading ? 'Deleting...' : 'Delete'}
    </button>
  );
}

// ✅ Call Server Action from onClick
// ✅ Handle loading state
// ✅ Error handling with try-catch
// ✅ User confirmation

Passing Arguments to Server Actions

Pattern 1: FormData (Forms)

TYPESCRIPT
'use server';

export async function createPost(formData: FormData) {
  // Extract from FormData
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;
  const published = formData.get('published') === 'on';
  
  await db.posts.create({
    data: { title, content, published },
  });
}

// Usage in form:
<form action={createPost}>
  <input name="title" />
  <textarea name="content" />
  <input type="checkbox" name="published" />
  <button>Submit</button>
</form>

// ✅ FormData for forms
// ✅ Automatic from form fields
// ✅ Works without JavaScript

Pattern 2: Regular Arguments

TYPESCRIPT
'use server';

export async function updatePost(
  postId: string,
  title: string,
  content: string
) {
  await db.posts.update({
    where: { id: postId },
    data: { title, content },
  });
  
  return { success: true };
}

// Usage programmatically:
await updatePost('post-123', 'New Title', 'New Content');

// ✅ Type-safe arguments
// ✅ Clean function signature
// ✅ Good for programmatic calls

Pattern 3: .bind() for Additional Arguments

TYPESCRIPT
'use server';

export async function updatePost(postId: string, formData: FormData) {
  const title = formData.get('title') as string;
  
  await db.posts.update({
    where: { id: postId },
    data: { title },
  });
}

// Usage with .bind() to pass postId:
<form action={updatePost.bind(null, 'post-123')}>
  <input name="title" />
  <button>Update</button>
</form>

// ✅ .bind() pre-fills first argument
// ✅ Useful for IDs with forms
// ✅ Maintains progressive enhancement

Pattern 4: Hidden Input Fields

TYPESCRIPT
'use server';

export async function updatePost(formData: FormData) {
  const postId = formData.get('postId') as string;
  const title = formData.get('title') as string;
  
  await db.posts.update({
    where: { id: postId },
    data: { title },
  });
}

// Usage with hidden input:
<form action={updatePost}>
  <input type="hidden" name="postId" value="post-123" />
  <input name="title" />
  <button>Update</button>
</form>

// ✅ Hidden inputs for IDs
// ✅ All data in FormData
// ✅ Works without JavaScript

Return Values and Responses

Returning Data

app/actions/posts.ts
'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  
  const post = await db.posts.create({
    data: { title },
  });
  
  // Return serializable data
  return {
    success: true,
    postId: post.id,
    message: 'Post created successfully!',
  };
}

// Usage:
const result = await createPost(formData);
console.log(result.message); // "Post created successfully!"

// ✅ Return plain objects
// ✅ Automatically serialized
// ✅ Type-safe

Error Handling

app/actions/posts.ts
'use server';

export async function createPost(formData: FormData) {
  try {
    const title = formData.get('title') as string;
    
    if (!title) {
      return {
        success: false,
        error: 'Title is required',
      };
    }
    
    await db.posts.create({
      data: { title },
    });
    
    return {
      success: true,
      message: 'Post created!',
    };
  } catch (error) {
    console.error('Failed to create post:', error);
    return {
      success: false,
      error: 'Failed to create post',
    };
  }
}

// Usage:
const result = await createPost(formData);
if (result.success) {
  alert(result.message);
} else {
  alert(result.error);
}

// ✅ Return success/error states
// ✅ Handle errors gracefully
// ✅ Informative error messages

Redirecting After Action

app/actions/posts.ts
'use server';

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

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  
  const post = await db.posts.create({
    data: { title },
  });
  
  // Revalidate blog page cache
  revalidatePath('/blog');
  
  // Redirect to new post
  redirect(`/blog/${post.id}`);
}

// ✅ redirect() after successful action
// ✅ revalidatePath() to update cached data
// ✅ Automatic navigation

Practical Examples

Example 1: Simple Todo App

app/actions/todos.ts
'use server';

import { revalidatePath } from 'next/cache';

export async function addTodo(formData: FormData) {
  const text = formData.get('text') as string;
  
  await db.todos.create({
    data: {
      text,
      completed: false,
    },
  });
  
  revalidatePath('/todos');
}

export async function toggleTodo(todoId: string) {
  const todo = await db.todos.findUnique({
    where: { id: todoId },
  });
  
  await db.todos.update({
    where: { id: todoId },
    data: {
      completed: !todo?.completed,
    },
  });
  
  revalidatePath('/todos');
}

export async function deleteTodo(todoId: string) {
  await db.todos.delete({
    where: { id: todoId },
  });
  
  revalidatePath('/todos');
}

// ✅ Three Server Actions for CRUD
// ✅ revalidatePath() to refresh data
// ✅ Simple, clean code
app/todos/page.tsx
import { addTodo, toggleTodo, deleteTodo } from '@/app/actions/todos';

async function getTodos() {
  return await db.todos.findMany();
}

export default async function TodosPage() {
  const todos = await getTodos();

  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Todos</h1>

      {/* Add form */}
      <form action={addTodo} className="mb-8">
        <input
          name="text"
          placeholder="What needs to be done?"
          required
          className="w-full px-4 py-2 border rounded"
        />
        <button
          type="submit"
          className="mt-2 px-6 py-2 bg-blue-600 text-white rounded"
        >
          Add Todo
        </button>
      </form>

      {/* Todos list */}
      <div className="space-y-2">
        {todos.map(todo => (
          <div
            key={todo.id}
            className="flex items-center gap-4 p-4 bg-white rounded shadow"
          >
            {/* Toggle form */}
            <form action={toggleTodo.bind(null, todo.id)}>
              <button
                type="submit"
                className={`w-6 h-6 border-2 rounded ${
                  todo.completed ? 'bg-blue-600 border-blue-600' : ''
                }`}
              >
                {todo.completed && '✓'}
              </button>
            </form>

            <span className={todo.completed ? 'line-through' : ''}>
              {todo.text}
            </span>

            {/* Delete form */}
            <form action={deleteTodo.bind(null, todo.id)} className="ml-auto">
              <button
                type="submit"
                className="text-red-600 hover:text-red-800"
              >
                Delete
              </button>
            </form>
          </div>
        ))}
      </div>
    </div>
  );
}

// ✅ Full CRUD without API routes
// ✅ Works without JavaScript
// ✅ Automatic data revalidation

Example 2: Contact Form

app/actions/contact.ts
'use server';

export async function submitContactForm(formData: FormData) {
  const name = formData.get('name') as string;
  const email = formData.get('email') as string;
  const message = formData.get('message') as string;

  // Validate
  if (!name || !email || !message) {
    return {
      success: false,
      error: 'All fields are required',
    };
  }

  // Save to database
  await db.contacts.create({
    data: { name, email, message },
  });

  // Send email notification
  await sendEmail({
    to: 'support@example.com',
    subject: 'New Contact Form Submission',
    body: `Name: ${name}
Email: ${email}
Message: ${message}`,
  });

  return {
    success: true,
    message: 'Thank you! We will get back to you soon.',
  };
}

// ✅ Complete form processing
// ✅ Validation
// ✅ Database + email
// ✅ Success/error responses
app/contact/page.tsx
'use client';

import { submitContactForm } from '@/app/actions/contact';
import { useState } from 'react';

export default function ContactPage() {
  const [result, setResult] = useState<{ success: boolean; message?: string; error?: string } | null>(null);

  async function handleSubmit(formData: FormData) {
    const response = await submitContactForm(formData);
    setResult(response);
  }

  return (
    <div className="max-w-2xl mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">Contact Us</h1>

      <form action={handleSubmit} className="space-y-4">
        <div>
          <label className="block font-semibold mb-2">Name</label>
          <input
            name="name"
            required
            className="w-full px-4 py-2 border rounded"
          />
        </div>

        <div>
          <label className="block font-semibold mb-2">Email</label>
          <input
            name="email"
            type="email"
            required
            className="w-full px-4 py-2 border rounded"
          />
        </div>

        <div>
          <label className="block font-semibold mb-2">Message</label>
          <textarea
            name="message"
            required
            rows={5}
            className="w-full px-4 py-2 border rounded"
          />
        </div>

        <button
          type="submit"
          className="px-6 py-3 bg-blue-600 text-white rounded hover:bg-blue-700"
        >
          Send Message
        </button>

        {result && (
          <div
            className={`p-4 rounded ${
              result.success ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
            }`}
          >
            {result.success ? result.message : result.error}
          </div>
        )}
      </form>
    </div>
  );
}

// ✅ Client Component for result display
// ✅ Server Action for submission
// ✅ Success/error feedback

Server Actions Project Structure

Organization of Server Actions in Next.js

appImportant

Select a file or folder to see details

Server Actions Best Practices

1. Organize Actions by Domain

BASH
// ✅ GOOD: Organized by feature
app/
├── actions/
│   ├── posts.ts      # Post actions
│   ├── users.ts      # User actions
│   ├── comments.ts   # Comment actions
│   └── auth.ts       # Auth actions

// ❌ BAD: Single actions file
app/
└── actions.ts        # Everything in one file

2. Return Structured Responses

TYPESCRIPT
// ✅ GOOD: Structured response
export async function createPost(formData: FormData) {
  try {
    // ...
    return { success: true, postId: post.id, message: 'Created!' };
  } catch (error) {
    return { success: false, error: 'Failed to create' };
  }
}

// ❌ BAD: Throwing errors
export async function createPost(formData: FormData) {
  // ...
  throw new Error('Failed'); // Hard to handle in UI
}

3. Use revalidatePath After Mutations

TYPESCRIPT
// ✅ GOOD: Revalidate after mutation
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
  await db.posts.create({ ... });
  revalidatePath('/blog'); // Refresh cached data
}

// ❌ BAD: No revalidation
export async function createPost(formData: FormData) {
  await db.posts.create({ ... });
  // Cache not updated - users see stale data
}

4. Add Server-Side Validation

TYPESCRIPT
// ✅ GOOD: Validate on server
export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  
  if (!title || title.length < 3) {
    return { success: false, error: 'Title must be at least 3 characters' };
  }
  
  // Continue...
}

// Never trust client-side validation alone

5. Use TypeScript for Type Safety

TYPESCRIPT
// ✅ GOOD: Type-safe
interface CreatePostResult {
  success: boolean;
  postId?: string;
  error?: string;
}

export async function createPost(formData: FormData): Promise<CreatePostResult> {
  // Implementation
}

// Full type safety from server to client

Key Takeaways

  • 'use server' - marks functions as Server Actions
  • Run on server - execute server-side automatically
  • Type-safe - full TypeScript support
  • Progressive enhancement - works without JavaScript
  • Co-located - define near components
  • FormData or arguments - flexible parameter passing
  • Return data - structured responses
  • Revalidation - update cached data after mutations

What's Next?

You've mastered Server Actions basics! Next, we'll explore Form Handling with Server Actions—building complete forms with progressive enhancement, handling different input types, file uploads, and creating accessible forms that work without JavaScript. You'll build production-ready forms!

We'll cover form structure, input types, accessibility, progressive enhancement patterns, and complete form examples with Server Actions.

🚀 Server Actions vs API Routes

Server Actions are the recommended way for mutations in Next.js 15. Use them instead of API routes for forms and data mutations. Reserve API routes for third-party webhooks, REST APIs for external clients, or when you need full control over HTTP methods and headers.

Test Your Understanding

Question 1 of 4

What is a Server Action?

Master Server Actions in Next.js! Learn to handle form submissions and mutations with type-safe server-side functions.

Previous
Working with Static Assets
Next
Form Handling with Server Actions

Master Next.js Forms

Join 2,000+ developers building powerful Next.js apps. Get the next lesson on form handling with Server Actions - 100% FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

NextJS Tutorials

0 of 70 completed

Your Progress0%

Topics

Getting Started

  • What is Next.js?
  • Next.js vs React - Key Differences
  • Creating Your First Next.js 15 Project
  • Next.js Project Structure Explained
  • App Router vs Pages Router

Routing Fundamentals

  • File-Based Routing Basics
  • Creating Pages with page.tsx
  • Dynamic Routes and Route Parameters
  • Catch-All and Optional Catch-All Routes
  • Route Groups for Organization
  • Parallel Routes
  • Intercepting Routes

Layouts and Pages

  • Understanding Layouts
  • Root Layout and Global Configuration
  • Nested Layouts
  • Templates vs Layouts
  • Loading States with loading.tsx
  • Error Handling with error.tsx

Server and Client Components

  • Understanding Server Components
  • Client Components with 'use client'
  • When to Use Server vs Client Components
  • Component Composition Patterns
  • Passing Props Between Server and Client
  • Server Component Patterns and Best Practices

Data Fetching

  • Fetching Data in Server Components
  • Parallel and Sequential Data Fetching
  • Data Fetching Patterns and Strategies
  • Caching and Revalidation
  • Revalidate and Cache Tags
  • Handling Loading and Error States in Data Fetching

Navigation and Links

  • Link Component Basics
  • useRouter Hook for Programmatic Navigation
  • usePathname and useSearchParams Hooks
  • Active Links and Navigation States
  • Redirects and Navigation Guards

Styling in Next.js

  • CSS Modules in Next.js
  • Tailwind CSS Setup and Configuration
  • Global Styles and CSS Variables
  • Next.js Font Optimization

Images and Media

  • Next.js Image Component Basics
  • Image Optimization and Best Practices
  • Working with Static Assets

Forms and Data Mutations

  • Understanding Server Actions
  • Form Handling with Server Actions
  • Form Validation and Error Handling
  • useFormStatus and useFormState Hooks
  • Revalidating Data After Mutations
  • Optimistic Updates

Metadata and SEO

  • Static Metadata Configuration
  • Dynamic Metadata Generation
  • Open Graph and Social Media Cards
  • Sitemap and Robots.txt

API Routes and Route Handlers

  • Route Handlers Introduction
  • GET and POST Request Handlers
  • Dynamic API Routes
  • Request and Response Objects
  • API Error Handling and Status Codes

Middleware and Advanced Features

  • Introduction to Middleware
  • Authentication with Middleware
  • Environment Variables and Configuration
  • Streaming and Suspense
  • Not Found and Global Error Pages

Deployment and Optimization

  • Understanding Static and Dynamic Rendering
  • generateStaticParams for Static Generation
  • Build and Production Optimization
  • Deploying to Vercel
  • Alternative Deployment Options
  • Performance Monitoring and Analytics

Real-World Projects

  • Project 1: E-commerce Product Catalog
  • Project 2: Dashboard with Authentication
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo